@fabricorg/platform-host 8.0.0 → 8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e81defa: Add `eventAppendChecks()`, `runEventAppendChecks(subject)`, and their public subject, check,
8
+ and result types for custom store authors. Six checks exercise first-write-wins event appends
9
+ through plain, outbox and transaction paths, including buffered and committed duplicates and
10
+ plain-to-outbox behavior. Each check uses earlier and later duplicates with changed envelope and
11
+ metadata fields, including added map keys. Whole-record comparisons normalize key order, dates
12
+ and nulls; numeric strings are normalised only for `sequence`, `attemptCount` and `leaseToken`.
13
+ The first returned records supply snapshot references, allowing custom envelope fields and fields
14
+ the store omits or returns as null, including required fields. Transaction checks verify after commit
15
+ without requiring read-your-writes. Each committed-duplicate transaction also appends a fresh plain
16
+ control event after the duplicate and verifies its presence after commit to detect a silently
17
+ rolled-back unit of work.
18
+ Outbox snapshots cover pending, probe-leased, retry-pending and published rows, including lease
19
+ fields. Retry-pending accepts cleared or expired leases or the probe owner retained without an
20
+ expiry; a probe lease still live more than five seconds past the probe's clock reports a failure
21
+ to release it, and an expiry inside that margin is waited out as clock skew. Attempt counts and error formatting are
22
+ not prescribed. Retry and retirement availability use whole seconds a few seconds in the past,
23
+ tolerating coarse timestamp precision and small clock skew. Lease tokens come from claims, not
24
+ optional list views.
25
+ Absence checks require a fresh control outbox row.
26
+
27
+ These checks detect common first-write-wins violations: throwing, replacing or merging events,
28
+ keeping both events, selecting the earlier-ordered or later-ordered duplicate, adding outbox rows,
29
+ and resetting or rewriting an existing outbox row in the probed states. They are not exhaustive.
30
+ Known limits are rules comparing ordering fields in mixed directions, store-wide uniqueness across
31
+ spaces or tenants, the dead-letter state, and claims leasing foreign rows without returning them.
32
+ The checks also cannot detect a buffering transaction that merges fields the store never returns
33
+ (such as `includeProvenance`) from a buffered duplicate, or duplicate handling backed by an
34
+ in-process cache that works within one run but not across restarts. Stores requiring gapless
35
+ sequences, or unique keys over fields the control events share with the probe event, can fail
36
+ falsely.
37
+
38
+ Missing append capabilities skip; missing outbox verification or cleanup methods (including
39
+ `markOutboxPublished`) fail. Run in test or staging with outbox relays paused for the probe tenant.
40
+ Event timestamps use whole seconds slightly in the past; probes leave append-only events in dedicated
41
+ `event-append-probe` spaces. Successful outbox-creating checks mark the row published without
42
+ sending anything; final cleanup attempts to dead-letter visible pending control/failure rows.
43
+ Published exercises duplicate appends against that delivery state. Absence-control rows remain
44
+ dead-lettered; operator requeue tooling could publish them later, so exclude probe spaces from real
45
+ publication and requeue workflows.
46
+
47
+ Claims use real current time, a pending-probe-row limit and a 30-second lease. A filter-ignoring
48
+ claim can still lease foreign rows and make its own writes, such as incrementing attempts. The
49
+ checks write nothing to foreign rows and stop claiming for that run; later outbox checks fail as
50
+ skipped because of the earlier filter violation. Standalone checks stop claiming for that check.
51
+ After a filter violation, cleanup dead-letters only probe rows returned by that claim, using the
52
+ probe worker and claim tokens without further claims. The failure lists visible probe rows still
53
+ pending and instructs manual dead-lettering, including unreturned rows or failed retirements.
54
+ Leases lapse within the requested duration if the store honours it. Visible relay interference
55
+ fails with instructions to pause relays. These are additive exports; Host and shipped-store runtime behavior is unchanged.
56
+
57
+ ### Patch Changes
58
+
59
+ - 1effb28: Document first-write-wins event appends for custom store authors. Host re-appends lifecycle events under stable IDs on retry and recovery, with a fresh sequence and timestamps and sometimes a different payload, so `appendEvent` and `appendEventWithOutbox` must treat an event ID the store already holds as a no-op that keeps the first event, without throwing, replacing or merging it. A store that refuses these appends can leave recovery unable to converge; custom stores that throw on a conflicting append need to change. The shipped memory and PostgreSQL stores already conform and enforce ID uniqueness across the whole store, not per tenant or space. `appendEventWithOutbox` for an ID already stored adds no outbox row, even when the first append had none, so `outbox.shouldPublish` should be deterministic for a given event ID. No runtime behavior changes.
60
+ - 5af8a02: Lifecycle record ids no longer collide when their key is cut to length (issue #90).
61
+
62
+ Host derives event, attestation, reconciliation and policy-evaluation ids from a key that it sanitises and used to cut to 96 characters. Two keys that differed only after the cut used to get the same id, and since stores keep the first write for an existing id, the later record was dropped silently. The realistic case was a vendor `externalReference` long enough to cut off the outcome, which merged a second `ExternalOperationContradicted` event into the first.
63
+
64
+ A key longer than 96 characters after sanitising is now cut to 64 characters followed by `_` and a digest of the whole raw key, 97 characters in all. Being one character over the cap, it cannot equal the id of any key that fits. Ids whose key fits are byte-identical to earlier releases.
65
+
66
+ Upgrade note: only ids whose composed key exceeds 96 characters change. Getting there takes a long vendor-supplied value, such as an `externalReference` or external operation id, or an unusually long policy id, vendor, provider, operation name or custom invocation id. An invocation that is in flight across the upgrade and re-appends such a record during retry or recovery writes it under the new id, next to the one written before the upgrade.
67
+
68
+ Not changed: keys within the limit that differ only in characters outside `[A-Za-z0-9_-]` (for example policy ids `a.b` and `a/b`) still share an id. Changing that would alter almost every existing id, because host's own keys use `:` and policy ids conventionally use `.`.
69
+
3
70
  ## 8.0.0
4
71
 
5
72
  ### Major Changes
package/README.md CHANGED
@@ -255,6 +255,19 @@ the adapter loop. Saga compensation is unchanged. Withheld-output and refused-ha
255
255
  reconciliation retain the degraded pattern. Adapter-row fencing and overdue-sweep/late-callback
256
256
  rules are described below.
257
257
 
258
+ Event appends are first-write-wins by event ID. Host re-appends lifecycle events under stable IDs
259
+ on retry and recovery, and each later attempt carries a fresh `sequence` and timestamps and can carry
260
+ a different payload. A custom store's `appendEvent` and `appendEventWithOutbox` must treat an ID it
261
+ already holds as a no-op that keeps the first event: do not throw, replace, or merge it. A store that
262
+ refuses these appends can leave recovery unable to converge, so the invocation stays `running` until
263
+ the recovery budget settles it, even when the adapter effect succeeded. Custom stores that currently
264
+ throw on a conflicting append need to change. The shipped stores enforce ID uniqueness across the
265
+ whole store, not per tenant or space. The rule covers the outbox too: `appendEventWithOutbox` for an
266
+ ID already stored adds no outbox row, including when the first append went through plain
267
+ `appendEvent`, so that event is never published later. Keep `outbox.shouldPublish` deterministic for
268
+ a given event ID. Custom store authors can verify these paths with the
269
+ [event append conformance checks](#event-append-conformance).
270
+
258
271
  ### Conditional adapter-row writes (8.0.0, issue #79)
259
272
 
260
273
  `PlatformHostStore.writeAdapterInvocation(input)` is optional. Memory and PostgreSQL implement it;
@@ -1069,6 +1082,88 @@ refusals and an admitted update in our scope. The two adapter isolation checks
1069
1082
  fail for stores that ignore scope. Deployments running `runTenantIsolationChecks` against a custom
1070
1083
  store will see new failures until that store honours the argument.
1071
1084
 
1085
+ ## Event append conformance
1086
+
1087
+ `eventAppendChecks()` and `runEventAppendChecks(subject)` run six checks for first-write-wins
1088
+ event IDs: plain appends, outbox appends, outbox-after-plain, transactional appends,
1089
+ transactional outbox appends, and transactional outbox-after-plain. Each sends earlier and later
1090
+ duplicates with lower and higher sequences, earlier and later occurrence/recording times, and
1091
+ different remaining envelope and outbox metadata fields. Transaction checks cover buffered
1092
+ duplicates and duplicates against a committed event; they do not require transaction reads to
1093
+ expose buffered writes. Each committed-duplicate transaction appends a fresh plain control event
1094
+ after the duplicate and verifies its presence after commit, detecting a silently rolled-back unit
1095
+ of work. Event-count checks count only the original probe ID; controls use distinct sequences.
1096
+
1097
+ The checks detect common first-write-wins violations: throwing, replacing or merging events,
1098
+ keeping both events, selecting the earlier-ordered or later-ordered duplicate, adding outbox
1099
+ rows, and resetting or rewriting an existing outbox row in the probed states. They are not
1100
+ exhaustive. Known limits include rules that compare ordering fields in mixed directions,
1101
+ store-wide uniqueness across spaces or tenants, the dead-letter state, and claims that lease
1102
+ foreign rows without returning them. They also cannot detect a buffering transaction that merges
1103
+ fields the store never returns (such as `includeProvenance`) from a buffered duplicate, or
1104
+ duplicate handling backed by an in-process cache that works during one run but fails across
1105
+ restarts. Stores requiring gapless sequences can fail falsely: the first event uses sequence 50
1106
+ in a fresh space. So can stores with a unique key over fields the control events share with the
1107
+ probe event, such as `actionInvocationId` with the subject.
1108
+
1109
+ Outbox-creating checks verify the first event and metadata, then compare whole-row snapshots
1110
+ while pending, leased by the probe worker, retry-pending, and settled as published. Retry-pending
1111
+ accepts a cleared lease, an expired lease with no owner or the probe worker, or the probe owner
1112
+ retained as a record without an expiry. A probe lease that still expires more than five seconds
1113
+ after the probe's clock fails with `markOutboxFailed did not release the lease`; an expiry inside
1114
+ that margin is treated as clock skew and waited out before the row is claimed again. A lease held
1115
+ by another worker fails with the request to pause relays. The checks prescribe neither a positive attempt
1116
+ count nor error formatting; they snapshot the values the store returns. Retry availability uses
1117
+ whole seconds a few seconds in the past, tolerating coarse timestamp precision and small clock
1118
+ skew, then the probe reclaims the row before publication. Snapshots include the full envelope,
1119
+ lease and publication fields. Comparisons sort object keys, normalize dates and date strings to milliseconds, treat null like undefined, and
1120
+ compare numeric strings in `sequence`, `attemptCount` and `leaseToken` as numbers. An absent
1121
+ actor disclosure is accepted; included disclosure or actor identity on the first outbox envelope
1122
+ fails because the first metadata does not opt in. A store's first returned records supply the
1123
+ snapshot references; extra envelope fields and fields the store omits or returns as null are
1124
+ accepted, including required fields. This tolerance does not validate envelope completeness. For
1125
+ buffered duplicates in the first transaction, exposed fields that differ between the first input
1126
+ and a duplicate must retain the first value. Outbox-after-plain checks write a fresh control
1127
+ event through the matching outbox seam and require its row to be the only outbox row visible in
1128
+ the probe space, so absence verification cannot pass solely because `listOutbox` returns
1129
+ nothing.
1130
+
1131
+ `EventAppendSubject<TDb>` supplies `store`, `tenantId`, and optional `reset()`. Only
1132
+ `appendEvent` and `listEvents(tenantId, spaceId)` are required; the subject does not need a full
1133
+ recoverable store. The result has `passed` and per-check `id`, `status`, `evidence`, and
1134
+ optional `error`, following the tenant-isolation suite. Absent outbox or transaction append
1135
+ capabilities produce `skipped: ` evidence; `passed` means no failures, so inspect skips for
1136
+ capability coverage. An available outbox append without `listOutbox`, `claimOutbox`,
1137
+ `markOutboxPublished` or `markOutboxFailed` fails because the check cannot verify or clean up.
1138
+ Append errors and probe retirement failures also fail.
1139
+
1140
+ Run against test or staging stores, preferably with a dedicated probe tenant. **Pause outbox
1141
+ relays for the probe tenant.** Event timestamps and written availability values (including
1142
+ retirement) use whole seconds a few seconds in the past. Claims and publication markers use
1143
+ real current time. Probe rows are expected to be due when availability follows `recordedAt`;
1144
+ stores assigning availability by their own clock may need additional setup. Visible foreign-worker
1145
+ leases or status changes before the probe acts fail with a request to pause relays. This detects visible interference; it does not prevent a relay race.
1146
+ Append-only events remain in fresh `event-append-probe-${uuid}` spaces. Successful
1147
+ outbox-creating checks leave their row **published although nothing was sent**; the space name
1148
+ identifies probe rows. Published is needed to exercise duplicate appends against that delivery
1149
+ state. Absence-control rows and failure rows instead go through final cleanup, which attempts
1150
+ to dead-letter visible probe rows still pending with `event append probe; never published`.
1151
+ Operator tooling can requeue those dead-lettered rows for real publication: exclude probe
1152
+ spaces from real publication and requeue workflows.
1153
+
1154
+ Claims use the real current time, a 30-second lease, and a limit equal to the pending probe row
1155
+ count. Cleanup filters listed rows locally by tenant and space and writes only to probe rows. If
1156
+ a claim returns foreign rows, the failure names their IDs, the checks write nothing to those
1157
+ rows, and no further claims occur in that run. Later outbox checks fail with an explanation that
1158
+ they were skipped because of the earlier filter violation; a standalone `check.run(subject)`
1159
+ stops claims for that check. Cleanup after a filter violation can dead-letter only probe rows
1160
+ returned by the violating claim, using the probe worker and that claim's tokens without another
1161
+ claim. The failure lists visible probe rows still pending, including unreturned rows or failed
1162
+ retirements, and instructs the operator to dead-letter them by hand. A filter-ignoring claim can
1163
+ still impose a short lease and make whatever writes the claim itself makes, such as incrementing attempt counts. Those leases lapse
1164
+ within 30 seconds of the claim if the store honours the requested duration; the checks do not
1165
+ release foreign leases or change foreign statuses, availability or errors.
1166
+
1072
1167
  ## Runtime observability and health
1073
1168
 
1074
1169
  Platform Host exposes vendor-neutral lifecycle telemetry through the optional `telemetry` sink on