@affordance/core 0.1.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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/engine/compute.d.ts +147 -0
  4. package/dist/engine/compute.js +152 -0
  5. package/dist/engine/compute.js.map +1 -0
  6. package/dist/engine/engine.d.ts +162 -0
  7. package/dist/engine/engine.js +115 -0
  8. package/dist/engine/engine.js.map +1 -0
  9. package/dist/engine/errors.d.ts +6 -0
  10. package/dist/engine/errors.js +11 -0
  11. package/dist/engine/errors.js.map +1 -0
  12. package/dist/engine/index.d.ts +16 -0
  13. package/dist/engine/index.js +15 -0
  14. package/dist/engine/index.js.map +1 -0
  15. package/dist/errors.d.ts +46 -0
  16. package/dist/errors.js +42 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/execution/delta.d.ts +42 -0
  19. package/dist/execution/delta.js +93 -0
  20. package/dist/execution/delta.js.map +1 -0
  21. package/dist/execution/errors.d.ts +94 -0
  22. package/dist/execution/errors.js +132 -0
  23. package/dist/execution/errors.js.map +1 -0
  24. package/dist/execution/execute.d.ts +179 -0
  25. package/dist/execution/execute.js +356 -0
  26. package/dist/execution/execute.js.map +1 -0
  27. package/dist/execution/index.d.ts +25 -0
  28. package/dist/execution/index.js +20 -0
  29. package/dist/execution/index.js.map +1 -0
  30. package/dist/execution/journal.d.ts +190 -0
  31. package/dist/execution/journal.js +192 -0
  32. package/dist/execution/journal.js.map +1 -0
  33. package/dist/execution/port.d.ts +81 -0
  34. package/dist/execution/port.js +101 -0
  35. package/dist/execution/port.js.map +1 -0
  36. package/dist/execution/replay.d.ts +58 -0
  37. package/dist/execution/replay.js +59 -0
  38. package/dist/execution/replay.js.map +1 -0
  39. package/dist/execution/timers.d.ts +21 -0
  40. package/dist/execution/timers.js +26 -0
  41. package/dist/execution/timers.js.map +1 -0
  42. package/dist/execution/transaction.d.ts +24 -0
  43. package/dist/execution/transaction.js +49 -0
  44. package/dist/execution/transaction.js.map +1 -0
  45. package/dist/guards/condition.d.ts +86 -0
  46. package/dist/guards/condition.js +11 -0
  47. package/dist/guards/condition.js.map +1 -0
  48. package/dist/guards/evaluate.d.ts +143 -0
  49. package/dist/guards/evaluate.js +167 -0
  50. package/dist/guards/evaluate.js.map +1 -0
  51. package/dist/guards/guard.d.ts +136 -0
  52. package/dist/guards/guard.js +118 -0
  53. package/dist/guards/guard.js.map +1 -0
  54. package/dist/guards/index.d.ts +20 -0
  55. package/dist/guards/index.js +17 -0
  56. package/dist/guards/index.js.map +1 -0
  57. package/dist/guards/time.d.ts +32 -0
  58. package/dist/guards/time.js +45 -0
  59. package/dist/guards/time.js.map +1 -0
  60. package/dist/index.d.ts +31 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/ingestion/correlation.d.ts +54 -0
  64. package/dist/ingestion/correlation.js +78 -0
  65. package/dist/ingestion/correlation.js.map +1 -0
  66. package/dist/ingestion/index.d.ts +16 -0
  67. package/dist/ingestion/index.js +15 -0
  68. package/dist/ingestion/index.js.map +1 -0
  69. package/dist/ingestion/ingest.d.ts +196 -0
  70. package/dist/ingestion/ingest.js +275 -0
  71. package/dist/ingestion/ingest.js.map +1 -0
  72. package/dist/migration/index.d.ts +10 -0
  73. package/dist/migration/index.js +10 -0
  74. package/dist/migration/index.js.map +1 -0
  75. package/dist/migration/migrate.d.ts +119 -0
  76. package/dist/migration/migrate.js +195 -0
  77. package/dist/migration/migrate.js.map +1 -0
  78. package/dist/model/casetype.d.ts +53 -0
  79. package/dist/model/casetype.js +51 -0
  80. package/dist/model/casetype.js.map +1 -0
  81. package/dist/model/errors.d.ts +28 -0
  82. package/dist/model/errors.js +39 -0
  83. package/dist/model/errors.js.map +1 -0
  84. package/dist/model/handler.d.ts +120 -0
  85. package/dist/model/handler.js +16 -0
  86. package/dist/model/handler.js.map +1 -0
  87. package/dist/model/index.d.ts +24 -0
  88. package/dist/model/index.js +19 -0
  89. package/dist/model/index.js.map +1 -0
  90. package/dist/model/retry.d.ts +40 -0
  91. package/dist/model/retry.js +51 -0
  92. package/dist/model/retry.js.map +1 -0
  93. package/dist/model/scope.d.ts +67 -0
  94. package/dist/model/scope.js +19 -0
  95. package/dist/model/scope.js.map +1 -0
  96. package/dist/model/step.d.ts +232 -0
  97. package/dist/model/step.js +210 -0
  98. package/dist/model/step.js.map +1 -0
  99. package/dist/model/target.d.ts +175 -0
  100. package/dist/model/target.js +236 -0
  101. package/dist/model/target.js.map +1 -0
  102. package/dist/store/bootstrap.d.ts +57 -0
  103. package/dist/store/bootstrap.js +268 -0
  104. package/dist/store/bootstrap.js.map +1 -0
  105. package/dist/store/errors.d.ts +18 -0
  106. package/dist/store/errors.js +26 -0
  107. package/dist/store/errors.js.map +1 -0
  108. package/dist/store/ids.d.ts +15 -0
  109. package/dist/store/ids.js +12 -0
  110. package/dist/store/ids.js.map +1 -0
  111. package/dist/store/index.d.ts +12 -0
  112. package/dist/store/index.js +9 -0
  113. package/dist/store/index.js.map +1 -0
  114. package/dist/store/queryable.d.ts +60 -0
  115. package/dist/store/queryable.js +7 -0
  116. package/dist/store/queryable.js.map +1 -0
  117. package/dist/store/resolve.d.ts +66 -0
  118. package/dist/store/resolve.js +71 -0
  119. package/dist/store/resolve.js.map +1 -0
  120. package/dist/store/sql.d.ts +26 -0
  121. package/dist/store/sql.js +21 -0
  122. package/dist/store/sql.js.map +1 -0
  123. package/dist/store/store.d.ts +66 -0
  124. package/dist/store/store.js +108 -0
  125. package/dist/store/store.js.map +1 -0
  126. package/package.json +61 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @affordance/core — the case framework's engine.
3
+ *
4
+ * A **case** is state plus independently-defined guarded **steps**; the
5
+ * engine computes the currently-available steps (**affordances**) from
6
+ * guards over state. No declared flow, no program counter;
7
+ * process changes deploy freely against in-flight cases.
8
+ *
9
+ * This barrel *is* the package's interface, and it is curated: the authoring
10
+ * API an app writes definitions with, the engine it runs them on, the error
11
+ * taxonomy an adapter translates, and the record types that cross the wire.
12
+ * The store loaders, the execution lifecycle's internals
13
+ * and the guard walk live behind the engine — submodules import them from
14
+ * each other's `index.js`, apps should not need to.
15
+ */
16
+ export type { Affordance, AffordanceExplanation, BlockedStep, CaseAffordances, CaseSnapshot, Engine, EngineOptions, ExplainOptions, } from './engine/index.js';
17
+ export { createEngine, UnknownCaseTypeError } from './engine/index.js';
18
+ export type { AffordanceErrorCode } from './errors.js';
19
+ export { AffordanceError, isAffordanceError } from './errors.js';
20
+ export type { ClaimedEntryInput, ClaimedJournalEntry, CompletedEntryInput, ExecuteOptions, ExecutionRecord, ExecutionResult, ExecutionStatus, FailureEntryInput, GuardReplay, JournalEntry, JournalEntryInput, JournalEntryType, JournalError, JournalFilter, PatchOp, StateDelta, } from './execution/index.js';
21
+ export { CaseBusyError, ClaimLostError, diffState, foldExecutions, isClaimedEntry, jsonEqual, replayGuard, StepExecutionError, StepNotAvailableError, stepLabel, } from './execution/index.js';
22
+ export type { AnyOfConditionResult, Condition, ConditionContext, ConditionMap, ConditionMapEntry, ConditionOutcome, ConditionResult, ConditionVerdict, Guard, GuardEvaluation, GuardEvaluationContext, GuardSection, Instant, SingleConditionResult, } from './guards/index.js';
23
+ export { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js';
24
+ export type { Correlation, CorrelationRegistration, DeadLetter, DeadLetterFilter, DeadLetterReason, ExternalActor, ExternalEvent, IngestionOptions, IngestionResult, IngestionStatus, } from './ingestion/index.js';
25
+ export { externalActor, routedStep } from './ingestion/index.js';
26
+ export type { MigrationFailure, MigrationOptions, MigrationProgress, MigrationReport, MigrationTransform, } from './migration/index.js';
27
+ 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 { CaseHandle, DatabaseAccess, Dormancy, PoolLike, Queryable, Transaction, } from './store/index.js';
31
+ export { bootstrap, CASE_TABLES, CaseNotFoundError, CaseStateValidationError, FRAMEWORK_SCHEMA, queryableOf, } from './store/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // Copyright © 2026 Mochicode LLC — mochicode.com
2
+ // ── The engine ─────────────────────────────────────────────────────────────
3
+ export { createEngine, UnknownCaseTypeError } from './engine/index.js';
4
+ // ── The error taxonomy: every deliberate refusal, one closed code set ──────
5
+ export { AffordanceError, isAffordanceError } from './errors.js';
6
+ // ── Execution records: what came back, what is journaled ───────────────────
7
+ export { CaseBusyError, ClaimLostError, diffState, foldExecutions, isClaimedEntry, jsonEqual, replayGuard, StepExecutionError, StepNotAvailableError, stepLabel, } from './execution/index.js';
8
+ // ── The authoring API: conditions and guards ───────────────────────────────
9
+ export { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js';
10
+ // ── Ingestion: events in, correlations, dead letters ───────────────────────
11
+ export { externalActor, routedStep } from './ingestion/index.js';
12
+ // ── Migration: the journaled restructure ───────────────────────────────────
13
+ export { hasMigrated, migrationStepName } from './migration/index.js';
14
+ // ── The authoring API: case types and steps ────────────────────────────────
15
+ // `step` itself stays behind the barrel: `caseType` requires a state schema,
16
+ // 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
+ // ── Persistence: what an app touches directly ──────────────────────────────
19
+ export { bootstrap, CASE_TABLES, CaseNotFoundError, CaseStateValidationError, FRAMEWORK_SCHEMA, queryableOf, } from './store/index.js';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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;AAmBhE,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;AAiB7B,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;AAsBrE,8EAA8E;AAC9E,6EAA6E;AAC7E,6EAA6E;AAC7E,OAAO,EACL,KAAK,EACL,QAAQ,EACR,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,wBAAwB,EACxB,OAAO,EACP,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AASzB,8EAA8E;AAC9E,OAAO,EACL,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,wBAAwB,EACxB,gBAAgB,EAChB,WAAW,GACZ,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 } 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 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 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 DEFAULT_RETRY,\n SCOPE_FAILURE_CONDITION,\n ScopeKeyError,\n StepInputValidationError,\n stepsOf,\n UnknownStepError,\n} from './model/index.js'\nexport type {\n CaseHandle,\n DatabaseAccess,\n Dormancy,\n PoolLike,\n Queryable,\n Transaction,\n} from './store/index.js'\n// ── Persistence: what an app touches directly ──────────────────────────────\nexport {\n bootstrap,\n CASE_TABLES,\n CaseNotFoundError,\n CaseStateValidationError,\n FRAMEWORK_SCHEMA,\n queryableOf,\n} from './store/index.js'\n"]}
@@ -0,0 +1,54 @@
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 type { CorrelationRequest } from '../model/handler.js';
19
+ import type { Queryable } from '../store/index.js';
20
+ /**
21
+ * A handler's {@link CorrelationRequest} with the case made explicit — what
22
+ * the registry actually stores. `step` is the step an event on this
23
+ * identifier should execute when the event does not name one itself:
24
+ * typically the materializing step, "record what the provider said".
25
+ */
26
+ export interface CorrelationRegistration extends CorrelationRequest {
27
+ /** The case the answer belongs to. */
28
+ readonly caseId: string;
29
+ }
30
+ /** A registered correlation as stored. */
31
+ export interface Correlation {
32
+ readonly id: string;
33
+ readonly system: string;
34
+ readonly externalId: string;
35
+ readonly caseId: string;
36
+ readonly scopeKey: string | null;
37
+ readonly step: string | null;
38
+ readonly metadata: unknown;
39
+ readonly createdAt: string;
40
+ }
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[]>;
@@ -0,0 +1,78 @@
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
+ };
78
+ //# sourceMappingURL=correlation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation.js","sourceRoot":"","sources":["../../src/ingestion/correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE5D,MAAM,YAAY,GAAG,GAAG,gBAAgB,eAAe,CAAA;AAoCvD,MAAM,aAAa,GAAG,CAAC,GAAmB,EAAe,EAAE,CAAC,CAAC;IAC3D,EAAE,EAAE,GAAG,CAAC,EAAE;IACV,MAAM,EAAE,GAAG,CAAC,MAAM;IAClB,UAAU,EAAE,GAAG,CAAC,WAAW;IAC3B,MAAM,EAAE,GAAG,CAAC,OAAO;IACnB,QAAQ,EAAE,GAAG,CAAC,SAAS;IACvB,IAAI,EAAE,GAAG,CAAC,IAAI;IACd,QAAQ,EAAE,GAAG,CAAC,QAAQ;IACtB,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE;CACxC,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EACtC,EAAa,EACb,YAAqC,EACf,EAAE;IACxB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B,eAAe,YAAY;;;;;;;uFAOwD,EACnF;QACE,MAAM,CAAC,aAAa,CAAC;QACrB,YAAY,CAAC,MAAM;QACnB,YAAY,CAAC,UAAU;QACvB,YAAY,CAAC,MAAM;QACnB,YAAY,CAAC,QAAQ,IAAI,IAAI;QAC7B,YAAY,CAAC,IAAI,IAAI,IAAI;QACzB,YAAY,CAAC,QAAQ,KAAK,SAAS;YACjC,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC;KAC1C,CACF,CAAA;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IACnB,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,eAAe,YAAY,kBAAkB,CAAC,CAAA;IACxE,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;AAC3B,CAAC,CAAA;AAED,uFAAuF;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,EAAa,EACb,MAAc,EACd,UAAkB,EACW,EAAE;IAC/B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B;YACQ,YAAY,yCAAyC,EAC7D,CAAC,MAAM,EAAE,UAAU,CAAC,CACrB,CAAA;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IACnB,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;AACtD,CAAC,CAAA;AAED,4FAA4F;AAC5F,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,EAAa,EACb,MAAc,EACd,QAAiB,EACgB,EAAE;IACnC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B;YACQ,YAAY;0BACE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB;6BAC/C,EACzB,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CACvD,CAAA;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;AAChC,CAAC,CAAA","sourcesContent":["/**\n * Correlation: the mapping from an external identifier to a case (and a\n * scope element within it) — CONTEXT.md.\n *\n * An e-sign provider knows envelope `env_9f2`; it does not know case\n * `4b0e…` or that the envelope is buyer #7's agreement. Something has\n * to hold that mapping, and it has to be the framework, because it is the\n * framework that has to route the resulting webhook to a step. This is that\n * something — one of exactly two integration primitives in core (the other\n * is {@link ingest}). There are no service-specific connectors here, and\n * there never will be: DocuSign's payload shape is the app's business.\n *\n * The registration is written by the handler that *initiates* the external\n * interaction, in the same commit that records having initiated it —\n * `ctx.correlate(...)` rides the commit seam, so a case can never be\n * left having sent an envelope it cannot route the answer for.\n */\n\nimport type { CorrelationRequest } from '../model/handler.js'\nimport type { Queryable } from '../store/index.js'\nimport { FRAMEWORK_SCHEMA, mintId } from '../store/index.js'\n\nconst CORRELATIONS = `${FRAMEWORK_SCHEMA}.correlations`\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\ntype CorrelationRow = {\n id: string\n system: string\n external_id: string\n case_id: string\n scope_key: string | null\n step: string | null\n metadata: unknown\n created_at: Date\n}\n\nconst toCorrelation = (row: CorrelationRow): Correlation => ({\n id: row.id,\n system: row.system,\n externalId: row.external_id,\n caseId: row.case_id,\n scopeKey: row.scope_key,\n step: row.step,\n metadata: row.metadata,\n createdAt: row.created_at.toISOString(),\n})\n\n/**\n * Register (or re-register) an external identifier against a case.\n *\n * Upserts on `(system, externalId)` — insert, or update the row already\n * there: a retried handler attempt registering the same envelope again is\n * not an error, it is the same fact. Pass any\n * {@link Queryable} — from a handler this is the commit transaction, via\n * `ctx.correlate` or `ctx.onCommit`.\n */\nexport const registerCorrelation = async (\n db: Queryable,\n registration: CorrelationRegistration,\n): Promise<Correlation> => {\n const { rows } = await db.query<CorrelationRow>(\n `insert into ${CORRELATIONS} (id, system, external_id, case_id, scope_key, step, metadata)\n values ($1, $2, $3, $4, $5, $6, $7::jsonb)\n on conflict (system, external_id) do update\n set case_id = excluded.case_id,\n scope_key = excluded.scope_key,\n step = excluded.step,\n metadata = excluded.metadata\n returning id, system, external_id, case_id, scope_key, step, metadata, created_at`,\n [\n mintId('correlation'),\n registration.system,\n registration.externalId,\n registration.caseId,\n registration.scopeKey ?? null,\n registration.step ?? null,\n registration.metadata === undefined\n ? null\n : JSON.stringify(registration.metadata),\n ],\n )\n const row = rows[0]\n if (!row) throw new Error(`insert into ${CORRELATIONS} returned no row`)\n return toCorrelation(row)\n}\n\n/** Look up where an external identifier routes; `null` when nothing has claimed it. */\nexport const lookupCorrelation = async (\n db: Queryable,\n system: string,\n externalId: string,\n): Promise<Correlation | null> => {\n const { rows } = await db.query<CorrelationRow>(\n `select id, system, external_id, case_id, scope_key, step, metadata, created_at\n from ${CORRELATIONS} where system = $1 and external_id = $2`,\n [system, externalId],\n )\n const row = rows[0]\n return row === undefined ? null : toCorrelation(row)\n}\n\n/** Every identifier registered against a case — the \"what is this case waiting on\" view. */\nexport const correlationsFor = async (\n db: Queryable,\n caseId: string,\n scopeKey?: string,\n): Promise<readonly Correlation[]> => {\n const { rows } = await db.query<CorrelationRow>(\n `select id, system, external_id, case_id, scope_key, step, metadata, created_at\n from ${CORRELATIONS}\n where case_id = $1 ${scopeKey === undefined ? '' : 'and scope_key = $2'}\n order by created_at asc`,\n scopeKey === undefined ? [caseId] : [caseId, scopeKey],\n )\n return rows.map(toCorrelation)\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The two integration primitives that live in core.
3
+ *
4
+ * - **Correlation** — external identifier ↔ (case, scope element). Written
5
+ * by the handler that initiates the external interaction.
6
+ * - **Ingestion** — `ingest(event)`: dedup, correlate, then an ordinary
7
+ * Execution with the external system as the actor (materialize-on-event).
8
+ *
9
+ * Deliberately absent, permanently: connectors. Nothing in this module knows
10
+ * what a DocuSign webhook looks like, and nothing in it ever will — the app
11
+ * owns payload shapes, the framework owns routing and exactly-once.
12
+ */
13
+ export type { Correlation, CorrelationRegistration } from './correlation.js';
14
+ export { correlationsFor, lookupCorrelation, registerCorrelation, } from './correlation.js';
15
+ 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, readDeadLetters, routedStep, } from './ingest.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The two integration primitives that live in core.
3
+ *
4
+ * - **Correlation** — external identifier ↔ (case, scope element). Written
5
+ * by the handler that initiates the external interaction.
6
+ * - **Ingestion** — `ingest(event)`: dedup, correlate, then an ordinary
7
+ * Execution with the external system as the actor (materialize-on-event).
8
+ *
9
+ * Deliberately absent, permanently: connectors. Nothing in this module knows
10
+ * what a DocuSign webhook looks like, and nothing in it ever will — the app
11
+ * owns payload shapes, the framework owns routing and exactly-once.
12
+ */
13
+ export { correlationsFor, lookupCorrelation, registerCorrelation, } from './correlation.js';
14
+ export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY, readDeadLetters, routedStep, } from './ingest.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ingestion/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AAazB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,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 {\n correlationsFor,\n lookupCorrelation,\n registerCorrelation,\n} 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 readDeadLetters,\n routedStep,\n} from './ingest.js'\n"]}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Ingestion: external events become Executions.
3
+ *
4
+ * A webhook arrives. Three things have to happen, in this order, and each of
5
+ * them has to be visible afterwards:
6
+ *
7
+ * 1. **Dedup.** Providers retry; "at least once" is the delivery guarantee
8
+ * every one of them offers. The gate is a unique insert into
9
+ * `ingested_events` — not a lookup-then-insert, which races itself — so
10
+ * three concurrent deliveries of one event produce exactly one Execution
11
+ * and two `duplicate` results.
12
+ * 2. **Correlate.** The external identifier is resolved to (case, scope,
13
+ * step) through the registry the initiating handler wrote.
14
+ * 3. **Execute.** Through the ordinary claim → run → commit, with the
15
+ * external system as the journaled actor. Ingestion has no privileged
16
+ * path: the guard still decides, transactionally.
17
+ *
18
+ * What is *not* allowed is a quiet drop. An event nothing can route, an event
19
+ * whose step the guard refuses, an event whose case is busy — each lands in
20
+ * the same table with a status and a reason, which is the dead-letter
21
+ * surface. "The webhook definitely arrived, so why is the case still waiting"
22
+ * is a question this table answers without anyone reading a log file.
23
+ */
24
+ import type { DeadLetterReason as ContractDeadLetterReason, IngestionStatus as ContractIngestionStatus } from '@affordance/contract';
25
+ import type { ExecutionEnvironment, ExecutionResult, SystemSettled } from '../execution/index.js';
26
+ import type { Queryable } from '../store/index.js';
27
+ import type { Correlation } from './correlation.js';
28
+ /** An event as an external system delivered it. */
29
+ export interface ExternalEvent {
30
+ /** The system that emitted it, matching the correlation's `system`. */
31
+ readonly system: string;
32
+ /** The identifier that routes it — the one the initiating handler registered. */
33
+ readonly externalId: string;
34
+ /** What happened, in the provider's own words: `'envelope.completed'`. */
35
+ readonly type: string;
36
+ /** The provider's delivery id, when it has one — the strongest dedup key. */
37
+ readonly eventId?: string;
38
+ /** The payload, passed to the target step as its input (validated by the step's schema). */
39
+ readonly payload?: unknown;
40
+ /** Execute this step instead of the correlation's registered one. */
41
+ readonly step?: string;
42
+ /** Override the derived idempotency key — for a provider whose retries are not identical. */
43
+ readonly idempotencyKey?: string;
44
+ /** When the external system says it happened (ISO-8601); recorded, never used as `asOf`. */
45
+ readonly occurredAt?: string;
46
+ }
47
+ /**
48
+ * How an ingested event ended up: `executed` (the target step ran and
49
+ * committed), `duplicate` (already seen — this delivery changed nothing, by
50
+ * design), or `dead-lettered` (nothing could be done with it, and it is
51
+ * sitting in the dead-letter surface). Wire vocabulary, so the contract
52
+ * declares it and this is the same closed set.
53
+ */
54
+ export type IngestionStatus = ContractIngestionStatus;
55
+ /**
56
+ * Why an event was dead-lettered — the operator's first question, answered.
57
+ *
58
+ * Two reasons are ingestion's own (`unrouted`: no correlation claims the
59
+ * identifier; `no-step`: routed, but neither the event nor the correlation
60
+ * names a step); every other reason **is** the `AffordanceErrorCode`
61
+ * the refused Execution already declared at its raise site. Ingestion does
62
+ * not re-derive the kind of a Refusal — it projects the code the error
63
+ * carries, so a new refusal class can never misroute here. Wire vocabulary,
64
+ * declared once by the contract; this is the same closed set.
65
+ */
66
+ export type DeadLetterReason = ContractDeadLetterReason;
67
+ /**
68
+ * Whether a provider redelivery of the same event deserves another attempt.
69
+ *
70
+ * Total over {@link DeadLetterReason}, so a new code cannot be added without
71
+ * deciding its reopen policy. `true` marks the outcomes that another delivery
72
+ * could genuinely cure — the case was busy, the handler crashed. Everything
73
+ * deterministic (a payload its schema rejects, an address that does not
74
+ * resolve, a guard that said no) stays dead-lettered: it will be refused the
75
+ * same way on every retry, so reopening it would only invite endless
76
+ * redelivery.
77
+ */
78
+ export declare const REOPENS_ON_REDELIVERY: Record<DeadLetterReason, boolean>;
79
+ /** What {@link ingest} resolves to — always a record, never a throw. */
80
+ export interface IngestionResult {
81
+ readonly id: string;
82
+ readonly status: IngestionStatus;
83
+ readonly system: string;
84
+ readonly externalId: string;
85
+ readonly idempotencyKey: string;
86
+ /** Where it routed, when it routed. */
87
+ readonly correlation: Correlation | null;
88
+ /** The Execution it produced, on `executed`. */
89
+ readonly execution: ExecutionResult | null;
90
+ /** Why it is in the dead-letter surface, on `dead-lettered`. */
91
+ readonly reason: DeadLetterReason | null;
92
+ /** Human-readable detail: the unmet conditions, the failure message. */
93
+ readonly detail: string | null;
94
+ readonly receivedAt: string;
95
+ }
96
+ /** A row of the dead-letter surface. */
97
+ export interface DeadLetter {
98
+ readonly id: string;
99
+ readonly system: string;
100
+ readonly externalId: string;
101
+ readonly type: string;
102
+ readonly idempotencyKey: string;
103
+ readonly caseId: string | null;
104
+ readonly scopeKey: string | null;
105
+ readonly step: string | null;
106
+ readonly reason: DeadLetterReason;
107
+ readonly detail: string | null;
108
+ readonly event: ExternalEvent;
109
+ readonly receivedAt: string;
110
+ }
111
+ /** Filters for {@link readDeadLetters}; all optional, all AND-ed. */
112
+ export interface DeadLetterFilter {
113
+ readonly system?: string;
114
+ readonly caseId?: string;
115
+ readonly reason?: DeadLetterReason;
116
+ readonly limit?: number;
117
+ }
118
+ /** How ingestion presents the external system to guards and the journal. */
119
+ export interface ExternalActor {
120
+ readonly kind: 'external';
121
+ readonly system: string;
122
+ readonly externalId: string;
123
+ readonly eventType: string;
124
+ }
125
+ /** Engine-level ingestion settings. */
126
+ export interface IngestionOptions {
127
+ /**
128
+ * Map an event to the Actor its Execution runs as. Defaults to an
129
+ * {@link ExternalActor}; an app whose `permits` conditions read its own
130
+ * actor shape supplies a mapping instead of bending that shape.
131
+ */
132
+ readonly actor?: (event: ExternalEvent) => unknown;
133
+ }
134
+ /** The normalized settings the environment carries. */
135
+ export interface IngestionSettings {
136
+ readonly actor: (event: ExternalEvent) => unknown;
137
+ }
138
+ /**
139
+ * What {@link ingest} needs from its caller: the execution environment plus
140
+ * ingestion's own settings. This is the widest environment any subsystem
141
+ * asks for, so it is also the shape the engine builds once and hands to all
142
+ * of them — named here so that contract is a type, not a coincidence of an
143
+ * object literal.
144
+ */
145
+ export interface IngestionEnvironment extends ExecutionEnvironment {
146
+ readonly ingestion: IngestionSettings;
147
+ }
148
+ export declare const externalActor: (event: ExternalEvent) => ExternalActor;
149
+ export declare const normalizeIngestion: (options?: IngestionOptions) => IngestionSettings;
150
+ /**
151
+ * The idempotency key an event dedups on.
152
+ *
153
+ * `(system, externalId, type)` is the key's fixed prefix, so the *same*
154
+ * notification about the *same* envelope produces the same key however many
155
+ * times it is delivered — and, because an external id resolves to one (case,
156
+ * scope element), the key is scoped exactly as far as the correlation is. The
157
+ * tail is the provider's own delivery id where there is one, and a hash of
158
+ * the payload where there is not: a provider that cannot tell you which
159
+ * delivery this is gets content-addressed dedup, which is the best anyone
160
+ * can do.
161
+ */
162
+ export declare const idempotencyKeyFor: (event: ExternalEvent) => string;
163
+ /**
164
+ * Which step an ingested event executes: the event's own naming wins,
165
+ * falling back to what the correlation registered. The precedence is stated
166
+ * once so any surface that *previews* routing (a dev console's world panel)
167
+ * asks the same question `ingest` will answer.
168
+ */
169
+ export declare const routedStep: (event: {
170
+ readonly step?: string;
171
+ }, correlation: {
172
+ readonly step: string | null;
173
+ }) => string | null;
174
+ /**
175
+ * Ingest one external event: dedup, correlate, execute — or dead-letter it
176
+ * with a reason.
177
+ *
178
+ * Never throws for an event's own sake: a webhook endpoint that 500s because
179
+ * a guard said no teaches the provider to retry something that will never
180
+ * succeed. Infrastructure failures (the database is gone) do still throw,
181
+ * because those the caller must not acknowledge.
182
+ */
183
+ export declare const ingest: (env: IngestionEnvironment, event: ExternalEvent) => Promise<IngestionResult>;
184
+ /**
185
+ * Project a settled system run onto the reason an operator needs to see —
186
+ * the one place that asks whether what settled was a Refusal.
187
+ *
188
+ * A framework Refusal already names its own kind and explains itself — the
189
+ * reason is its code, the detail its message (a refused guard's message
190
+ * carries the unmet conditions, addressed). Anything else is a bug, not a
191
+ * Refusal; it lands as `execution-failed` so the event is kept, never
192
+ * silently dropped, with the crash's own words as the detail.
193
+ */
194
+ export declare const classifyDeadLetter: (ran: SystemSettled) => [DeadLetterReason, string];
195
+ /** Read the dead-letter surface, newest first — the ops view of "arrived, did nothing". */
196
+ export declare const readDeadLetters: (db: Queryable, filter?: DeadLetterFilter) => Promise<readonly DeadLetter[]>;