From 4a7267040751f1fa34a93af3c2782f0d69e41c91 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:47:16 -0400 Subject: [PATCH] feat(storage-bench): storage/DB backend perf + integrity benchmark, aligned to the storage standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A benchmark harness for storage backends where INTEGRITY is a first-class result, not a footnote — aligned to SocioProphet/socioprophet-standards-storage (evidence-first: a number is a claim only if backed by a reproducible record). - ts/src/storage-bench.ts: benchObjectBackend measures any ObjectBackend (InMemory/S3-BYOS/RocksDB/…) on put/get/verify — latency p50/p95/p99 + throughput (ops/s, MB/s) — over deterministic seeded content-addressed blobs. FAIL-CLOSED: a round-tripped blob that doesn't byte-match, or whose content hash doesn't re-derive, makes the whole run a failure. toEvaluationRecord emits a record conforming to evaluation-record-standard.v1 (subject_type=platform_capability, attempt_mode=benchmark_run), carrying the raw measurements as evidence. - ts/src/storage-bench.test.ts: 5 tests, teeth both ways — a correct backend passes with full integrity + ordered percentiles; the run is deterministic for a fixed seed; a corrupting backend (wrong bytes) FAILS; a losing backend (missing blob) FAILS; the EvaluationRecord conforms. - docs/STORAGE_BACKEND_MATRIX.md: the Table-2.1 analog — estate backends (Canonical object store, S3-BYOS, RocksDB, Hypercore, AtomSpace, FederatedAtomSpace) + external field, scored on the STANDARD's dimensions (content-addressed / provenance / integrity-at-rest / fault tolerance / sovereign-BYOS / distributed / fail-closed verify) — perf numbers live in EvaluationRecords, not the table. Typecheck clean; storage-bench suite 5/5; dist rebuilt. --- docs/STORAGE_BACKEND_MATRIX.md | 63 +++++++++++ ts/dist/index.d.mts | Bin 238108 -> 240241 bytes ts/dist/index.d.ts | Bin 238108 -> 240241 bytes ts/dist/index.js | Bin 433227 -> 437391 bytes ts/dist/index.mjs | Bin 424201 -> 428236 bytes ts/src/index.ts | 1 + ts/src/storage-bench.test.ts | 66 +++++++++++ ts/src/storage-bench.ts | 195 +++++++++++++++++++++++++++++++++ 8 files changed, 325 insertions(+) create mode 100644 docs/STORAGE_BACKEND_MATRIX.md create mode 100644 ts/src/storage-bench.test.ts create mode 100644 ts/src/storage-bench.ts diff --git a/docs/STORAGE_BACKEND_MATRIX.md b/docs/STORAGE_BACKEND_MATRIX.md new file mode 100644 index 0000000..30870ec --- /dev/null +++ b/docs/STORAGE_BACKEND_MATRIX.md @@ -0,0 +1,63 @@ +# Storage & DB backend matrix + +A selection of storage / DB backends and their properties — the estate's own backends first, then the +external field for context. The dimensions are **the SocioProphet storage-standard properties**, not a +generic feature list: content-addressing, provenance, integrity-at-rest, fault tolerance, and +sovereignty are what the standard load-bears on. `(✓)` = supported via an adapter / optional dependency. + +> Measure it or don't claim it. Perf numbers for any row belong in a reproducible **EvaluationRecord** +> (SocioProphet/socioprophet-standards-storage `evaluation-record-standard.v1`), produced by +> `ts/src/storage-bench.ts` — never inline in this table. + +## Estate backends + +| Backend | Model | Content-addressed | Provenance-stamped | Integrity-at-rest | Fault tolerance | Sovereign / BYOS | In-memory | Distributed | Fail-closed verify | +|---|---|---|---|---|---|---|---|---|---| +| `InMemoryObjectBackend` | blob KV | ✓ (sha256 key) | (via store) | (via codex) | – | – | ✓ | – | ✓ (bench) | +| `CanonicalObjectStore` | content-addressed object store | ✓ | ✓ (`ObjectProvenance`) | ✓ (**codex-seal** at ingest) | (backend) | (backend) | (backend) | (backend) | ✓ (`verify → Syndrome`) | +| `S3ObjectBackend` (BYOS) | object store on S3/MinIO | ✓ | (via store) | (via codex) | ✓ (S3/MinIO) | ✓ (customer holds bytes) | – | ✓ | ✓ (bench) | +| `RocksDBBackend` | embedded LSM KV (AtomSpace WAL) | ✓ (atom handle) | ✓ (log entry) | ✓ (replay) | ✓ (WAL) | ✓ (local-first) | (cache) | – | ✓ | +| `HypercoreBackend` | signed append-only log | ✓ | ✓ (signed) | ✓ (merkle) | ✓ (replicated) | ✓ (per-writer keys) | – | ✓ (Autobase) | ✓ | +| `AtomSpace` (metagraph) | typed hypergraph, PLN truth | ✓ (structural hash) | ✓ (Values) | ✓ (codex sealer) | (backend) | ✓ | ✓ | (super-peer) | ✓ | +| `FederatedAtomSpace` | Autobase-merged sovereign logs | ✓ | ✓ (per-op writer+seq) | ✓ (causal-cut proof) | ✓ (multi-writer) | ✓ (federation) | – | ✓ | ✓ (proof withholds) | + +## External field (context) + +| Backend | Model | Content-addressed | Provenance | Fault tolerance | In-memory | Distributed | Language | +|---|---|---|---|---|---|---|---| +| RocksDB | embedded LSM KV | – | – | ✓ (WAL) | (cache) | – | C++ | +| Neo4j | property graph | – | – | ✓ | (cache) | ✓ | Java | +| Kùzu | embedded graph | – | – | ✓ | ✓ | – | C++ | +| Redis | in-memory KV | – | – | (✓) | ✓ | ✓ | C | +| S3 / MinIO | object store | (by key) | – | ✓ | – | ✓ | Go / — | +| Postgres | relational | – | – | ✓ | (cache) | (✓) | C | +| IPFS | content-addressed | ✓ | – | ✓ | – | ✓ | Go | + +## What the estate adds over the field + +The standard-aligned columns are exactly where the estate backends differ from the external field: +**content-addressing + provenance + integrity-at-rest (codex-seal) + causal-cut proof** are first-class, +not bolt-ons. A backend that returns the wrong bytes fast is a *failure* here — see the fail-closed +integrity check in `storage-bench.ts` (a corrupting/losing backend fails the run, proven both ways in +`storage-bench.test.ts`). + +## Running the benchmark + +```ts +import { InMemoryObjectBackend } from './object-store.js' +import { benchObjectBackend, toEvaluationRecord } from './storage-bench.js' + +const result = await benchObjectBackend(new InMemoryObjectBackend(), + { backend_id: 'in-memory', n_ops: 10_000, payload_bytes: 4096, seed: 42 }) + +// store as evidence — a number is only a claim if it is backed by a reproducible record. +const record = toEvaluationRecord(result, { + subject_ref: 'hellgraph:CanonicalObjectStore', + evaluation_track_ref: 'track:storage-backend-perf.v1', +}) +``` + +The `EvaluationRecord` conforms to SocioProphet/socioprophet-standards-storage +`standards/evaluation-record-standard.v1` (`subject_type: platform_capability`, +`attempt_mode: benchmark_run`), carrying the raw measurements as evidence so results are storable, +reviewable, and regression-checkable across epochs. diff --git a/ts/dist/index.d.mts b/ts/dist/index.d.mts index 3daa451284d7bb89a8520561725206cdd3aaf889..73307aa695df0040ff6b692e36eea217d9818ae0 100644 GIT binary patch delta 1921 zcmZ{lO^+Kz5QeooQ3M8}M1q7w3RJ|w_F^Z2WDoI1IfM^!KnmI%kXWJKp6MP>$1~HD z?jA3rWMvR5AtVk+27EzC`431waD)SJ<_CZ{<;Xvvx@Tr>&pLbYy87#_dY>x)_}iKP z{yOvd(Z#4!-{ol#y(8054B=4=@Xx2gh^LGQHUMpuNQXNv zo)Veq0i=22b5({iTEvnv;unTDmac7W0ps{sB2uA^2taerZiALULFFk34T@(}|Fld`Jz-F76v@bfr4mzD|;chV0h% zn%J!qv4nnhGRA3&!P&#!=J_}8WxHJGv8mpY*#O#EZrbn|+CyG`j=2(H(Vm`r;}zeQ zF{C2SQTb}c;!^nlmQyH;X(A=_VTAYELX*b9rPahIrLM`chbs!7Kl(K{UPc zEP{MgXDk;9ryBb&L4VMYlj2K24r|C0YeQgP!QCY)^-@U~(RqT;6w_b-JU3|& zXVD`E!bAkYVmL8N5bf(sxSHU_ZjiABp>}v6E;wwK?uafTohmRG_u#a5bs_p@*F;WfR{_*f_ zS_++7RX-F;BiITD1A>pH_k)NBBZ`x}JH7GG`L~R0p6;l1HuSLNtwE1wKY1Iky}b^Y zMc#*qWAobi--WmR{Bt#Y^V45?ub+Bh=?7wd`71Bne93|a^=O-a@wNBI>FX}Gj7IrG mhz)BuFE8x6DKr1zTkq)9*=AqOfB(*tFP?2Go85TgJ^dfZC7cQX delta 56 zcmV-80LTCF)DE1)4uFIKgaWh!<9WBGfdeuLx9*Aq5Cpdoj{{x;w|GYYdl+sRQMiAQo=` diff --git a/ts/dist/index.d.ts b/ts/dist/index.d.ts index 3daa451284d7bb89a8520561725206cdd3aaf889..73307aa695df0040ff6b692e36eea217d9818ae0 100644 GIT binary patch delta 1921 zcmZ{lO^+Kz5QeooQ3M8}M1q7w3RJ|w_F^Z2WDoI1IfM^!KnmI%kXWJKp6MP>$1~HD z?jA3rWMvR5AtVk+27EzC`431waD)SJ<_CZ{<;Xvvx@Tr>&pLbYy87#_dY>x)_}iKP z{yOvd(Z#4!-{ol#y(8054B=4=@Xx2gh^LGQHUMpuNQXNv zo)Veq0i=22b5({iTEvnv;unTDmac7W0ps{sB2uA^2taerZiALULFFk34T@(}|Fld`Jz-F76v@bfr4mzD|;chV0h% zn%J!qv4nnhGRA3&!P&#!=J_}8WxHJGv8mpY*#O#EZrbn|+CyG`j=2(H(Vm`r;}zeQ zF{C2SQTb}c;!^nlmQyH;X(A=_VTAYELX*b9rPahIrLM`chbs!7Kl(K{UPc zEP{MgXDk;9ryBb&L4VMYlj2K24r|C0YeQgP!QCY)^-@U~(RqT;6w_b-JU3|& zXVD`E!bAkYVmL8N5bf(sxSHU_ZjiABp>}v6E;wwK?uafTohmRG_u#a5bs_p@*F;WfR{_*f_ zS_++7RX-F;BiITD1A>pH_k)NBBZ`x}JH7GG`L~R0p6;l1HuSLNtwE1wKY1Iky}b^Y zMc#*qWAobi--WmR{Bt#Y^V45?ub+Bh=?7wd`71Bne93|a^=O-a@wNBI>FX}Gj7IrG mhz)BuFE8x6DKr1zTkq)9*=AqOfB(*tFP?2Go85TgJ^dfZC7cQX delta 56 zcmV-80LTCF)DE1)4uFIKgaWh!<9WBGfdeuLx9*Aq5Cpdoj{{x;w|GYYdl+sRQMiAQo=` diff --git a/ts/dist/index.js b/ts/dist/index.js index e258c0931f3c78a44470bda95d2005e3686a3681..b97460ccfb25f62bbe6a0325db4ac1687139895e 100644 GIT binary patch delta 3919 zcmbVP&2Jk;6jvPML#YI)s1+?A57Uoz;%*%$ZWOm^t3oRTr9~i4A$2C+alEO$yPcU$ zV^V9yfsb4OsfQID;L)4R`?!EW!jeT=3xAEavHGX+BcLg8u zX71Qn^@BT`IsA>|FTZtX^O5h~AFFPyKVJAPhtJFV3L=ls9}g5Rjp1|eQ-!(lN2_+z z_mp5wC2xIwdVl;+v2Y;1`E=nna=n{RoNwYa4j$%PJAuueuu2V4ag zCZKw{&If-`5%K!q4vz5t83SCPANY89WSwY^iYIyq^K0;;+?kAF^|hN)?i z8c;~j)a(cenQAEkk4dYRK&Ka07tzW(9EEDF7Js>!e=gDqvLx#U9WISIw*}Vo3B~Sk zZ$-6BX+Ad~s8t$WS%*p)g1Or8buNAFvh~3$XP$Y7J~?5sM*v!|ZJSI)Xm!z+zEGCM z%AiXE436X5P~bWxGN5XKTzHMCw%v6-OD4V{^Hdo$xz+J~VFj4$k<(JCv`E?#Ol;_M zm^gSHu3HtmRw^4}5)sknhC~yaCe9_CX_Rsy>&K!|(@o<}vKW8yQ~uBroPg1+qDf3k ze@`p)n#%P(@PjwF3_Hs5gEDlODnpC8oltOzsp%;$FR&)of&wt9RK8-J*C?ULt*u0G z^CnE~h!z#<0God3sXB2d*)!yU8O?;J+-vsgrh6>>OoCQz;fE{jAXE)MkPSq^P!UE) z-bCci1WfU1qd6neUEU@!F$@Z&t*J_NhD7mEO0*4>rKw`F=QUxFux0eKePwx-H`RI8 zyv9A}RcqP2lqp3mcO{!H-Hh|efvvyJo`q^K&+2$Iv z8bpQ(Q*epV5Ln0%SNJZ(H7>H8)aodF5$lT5*hDClevl7z?s|%^2v_wQD169mxi~^J z@9@ABEdJ`l+_QU4oVG_biQi@8`y!1M6rl)r8E1(=V8&cSbL)0XQ$Qh;xS5q!mceAh z4no;`icBt=8LN~=xqUW%9g z9N+(9k}xyf&3p&SA|}>&GQ5U%n#B2_Dy0~<@=xc<<6D1xvp@cbjTa)m9w2aZa{q6p zl87G`FjaNMS5#ntH2S0TfmF=dXc7<-Itg4ZxQiwQ-0-dWu9MDly5&$_WA49Z!8o7? zJSt3Y7p;Xr7?&R#KYUQF=_>9LQ~b;A{Ju!=ZTKKnMzaC?fAeN7ks)tVmGkpp0EU6X z(W8_d8&?h1kycu)QdAmk5ZxasGje z#qOtJfQ;6O2{_k?zq^~WFbR6U*w95t(A#jGdOBQ&q;&AoFsj1_;LT?m5cdjN4*G@x zo3{=NS{)28f+9Ls#G6)QT|de3GDN%fu+fwiM|hyP0m)$2q@&Ruod7$fv7BzJhYCzx z%QUZy;b@zz_B*-#hEqz*`AuW)k zM$bby3&ns*S%f+i$!yWs#I+|G^5#2JXbffaR#yInHP#6kUD0pwrY{^ztd`Lwl`KqX z5SK!%Q58WVY^`W(yh_0HlWMLVX8sZnnvuq+P|pz_6MUl=<#iZ{a95=W;5Z`P&q z1zX|A^<7v$ydmr%_WJsJ7;JyZgiEp}payN(*BXMi>N+BOx1;i&h-Qrp^-Y^$&QOxP zCM>EK;A&M2a6h06pwVOjTXs8mrzHhoO7U)>NEhLL19VIYb!QY2hBXofqzbyeLzZ;( zT_!M;&_l%H3MR3?yWCT#UqFAbnknHnQ?pbN+pMuDQ^a&o3h_h$9`@KM!w*)ymWi1ZbbuZImaN*_c+^&=O~D z!wr%U$%gs>D0A1jAC?7*^BWENM}~ml2-d=xrw39~a3s?7MhIl^ zV@|`s8JyyBQr!bv-@Aq4<43mdl-Q&@7tO6-dxa_-**+SOcjvs=`uo$uo4J1hyw3C$ delta 142 zcmV;90CE41*&55Z8j#U}`GxrbwfO>fh?jAR0#BF0i2^5w>4^e|>4^h}>4^ll>4^op zhPR{;2G#<%dlLq71h?)N2226BI~fK!0}3uH}2MU;)FP)K?usFD5w$=BBaib_dDL!-rel~ znj^gq&XN+-SL=h3eN2k zI3*Cy>%0g%@egn1$7f%51#59r$>zrZI$XjQ`6G3>1`eEu>P&5Vda5=%Qz^owOP8QL zKNkP_@#tfmtmfhRcrZ+!`wRc41os8mY@xV%7a$-)ce^7R!A zVGGVewN{H)KRxh7BqWo<6s+raxX{}*gO{~QZ8tPO^PqNO7lu*=GyF~ED8&J6mEjm;Hn_lS3I%y}_ z)8hU%N(ooF+uW{$CI@~dpk+Jo!u7Tv%7*8Q2C`t_2+>hAuTnVYp~9!M=Zr~rZI8wI z0VuRKE9L4mnc}0Ec zWWiEji;GY#PtVN^=!GZ)$?XPa@bI@Ivr(IgHdiE7tkIc9%vYEs=4OQJd>`Q^53-tc z@X`1(780%T)SyxNK{3#}<4V3BIC8sz#^X-Q!4{!(2MfAF@x!-9CJt(xmWxG*e|^$8 z8Yym|1p(ZrF(vxGZdeVat&UepBSK8(y6c%)1)U7b4@KLg_aQr3Mk}8m?ss;}^=_KQ z`FQDt{IO$6UC>r4(}ctE;+qE^eG1Z~Oiof3u)OM5r&({8xiPd~Jn1rCDvVC#DU$fd z&vPduzU3noRKET%`UJ&Z5Go$3&blXch%(qt`Z16Yv9l95nIj=ZiltRZxmb2q6wYg@ z=BDS^X(!fDHF@p=hEJSC|Qcf(s5weaj)3Owc!{k=nPD{=^f@hY~CZv}sNjh%N zr}IKxkPs$EX);X$0YJS&c(Jq`*TJKkp5#h2)bNucH5|L1HiUsF?T*wy>UhiH^`uV? zUh8yW_p}0by*ex_X5fXIcIyHLRlw^|wFvd3fJ+c*7uN8>)1I>CSWYz8eCYRww4L48 z8f)jYw$XtuVu^Z;-ZVyU8c{1| z#4`)iN^?{atR=n6&K9@Lsba5-104}dD-)o5>-NE@(E49AE~t*$yQrdz)BvOg1Cf#_ zJ-7k>XGBLhA-Otk|8eg4rekB^ys;L#c86;wA(OTA9#rOz{a;)T(460FAUh-x!46mp zo1W@O&48njx;9cE!ymmG`gVU6r;F+s>^{Ve&y2%+=fK@NHw^E=!+YC63|q!;{{qFo BstW)B delta 170 zcmX>zQL6Kk5$3wjB=J V*Ojm>W80ol&6dqG{dqgvQver(I { + const r = await benchObjectBackend(new InMemoryObjectBackend(), CFG) + assert.equal(r.result, 'pass') + assert.equal(r.integrity_verified, CFG.n_ops) + assert.equal(r.integrity_failures, 0) + const put = r.ops.find((o) => o.op === 'put')! + assert.equal(put.count, CFG.n_ops) + assert.ok(put.latency.p95_ms >= put.latency.p50_ms) // percentiles ordered + assert.ok(put.throughput_ops_s > 0 && put.throughput_mb_s > 0) +}) + +test('the run is deterministic for a fixed seed (reproducible — the standard requires it)', async () => { + const a = await benchObjectBackend(new InMemoryObjectBackend(), CFG) + const b = await benchObjectBackend(new InMemoryObjectBackend(), CFG) + assert.equal(a.integrity_verified, b.integrity_verified) + assert.equal(a.result, b.result) +}) + +// a fast liar: stores nothing and returns wrong bytes. It MUST fail the bench. +class CorruptingBackend implements ObjectBackend { + async put(): Promise {} + async get(): Promise { return Buffer.from('not the bytes you stored') } +} + +test('a backend that returns the wrong bytes FAILS (integrity is first-class, teeth both ways)', async () => { + const r = await benchObjectBackend(new CorruptingBackend(), CFG) + assert.equal(r.result, 'fail') + assert.ok(r.integrity_failures > 0) + assert.equal(r.integrity_verified, 0) +}) + +// a backend that loses data (get returns undefined) is also a failure, counted as a get failure. +class LosingBackend implements ObjectBackend { + async put(): Promise {} + async get(): Promise { return undefined } +} + +test('a backend that loses data FAILS (missing blob is not a silent pass)', async () => { + const r = await benchObjectBackend(new LosingBackend(), CFG) + assert.equal(r.result, 'fail') + const get = r.ops.find((o) => o.op === 'get')! + assert.equal(get.failures, CFG.n_ops) +}) + +test('toEvaluationRecord conforms to the storage standard shape', async () => { + const r = await benchObjectBackend(new InMemoryObjectBackend(), CFG) + const rec = toEvaluationRecord(r, { + subject_ref: 'hellgraph:InMemoryObjectBackend', + evaluation_track_ref: 'track:storage-backend-perf.v1', + }) + assert.equal(rec.subject_type, 'platform_capability') + assert.equal(rec.attempt_refs[0]!.attempt_mode, 'benchmark_run') + assert.ok(['pass', 'fail'].includes(rec.result)) + assert.equal(rec.review_state, 'draft') + for (const k of ['id', 'evaluation_track_ref', 'subject_ref', 'evidence_bundle_ref', 'created_at', 'updated_at']) + assert.ok((rec as Record)[k], `missing required field ${k}`) + // the raw, reproducible measurements are carried as evidence (no bare numbers). + assert.equal(rec.attempt_refs[0]!.measurements.integrity_verified, CFG.n_ops) +}) diff --git a/ts/src/storage-bench.ts b/ts/src/storage-bench.ts new file mode 100644 index 0000000..e31c317 --- /dev/null +++ b/ts/src/storage-bench.ts @@ -0,0 +1,195 @@ +/** + * storage-bench — a performance + integrity benchmark harness for storage / DB backends, + * aligned to the SocioProphet storage standard (evidence-first: a number is only a claim if it is + * backed by a reproducible record). + * + * It measures any `ObjectBackend` (InMemory / S3-BYOS / RocksDB / …) on the operations a + * content-addressed store actually performs — put, get, and verify (get + re-hash) — reporting + * latency percentiles and throughput. Integrity is a FIRST-CLASS result, not a footnote: if a + * round-tripped blob does not byte-match, or its content hash does not re-derive, the run is a + * FAILURE. A fast backend that returns the wrong bytes fails here; that is the point. + * + * `toEvaluationRecord` emits a record conforming to + * SocioProphet/socioprophet-standards-storage `standards/evaluation-record-standard.v1.md` + * (subject_type=platform_capability, attempt_mode=benchmark_run) so a result is storable, + * reviewable, and regression-checkable — never a bare number. + */ +import { createHash } from 'node:crypto' +import { performance } from 'node:perf_hooks' +import type { ObjectBackend } from './object-store.js' + +export interface StorageBenchConfig { + backend_id: string + n_ops: number + payload_bytes: number + /** deterministic seed so a run is reproducible (the storage standard requires it) */ + seed?: number +} + +export interface LatencyStats { + p50_ms: number + p95_ms: number + p99_ms: number + max_ms: number + mean_ms: number +} + +export interface OpResult { + op: 'put' | 'get' | 'verify' + count: number + failures: number + latency: LatencyStats + throughput_ops_s: number + throughput_mb_s: number +} + +export interface StorageBenchResult { + backend_id: string + config: StorageBenchConfig + ops: OpResult[] + integrity_verified: number + integrity_failures: number + /** fail-closed: any integrity failure (or op failure) makes the whole run a failure */ + result: 'pass' | 'fail' + started_at: string + finished_at: string +} + +const sha256 = (b: Buffer): string => createHash('sha256').update(b).digest('hex') + +/** Deterministic pseudo-random bytes from a seed, so a run reproduces exactly. */ +function seededPayload(seed: number, i: number, size: number): Buffer { + const out = Buffer.allocUnsafe(size) + let x = (seed ^ (i * 2654435761)) >>> 0 + for (let j = 0; j < size; j++) { + x ^= x << 13; x ^= x >>> 17; x ^= x << 5; x >>>= 0 + out[j] = x & 0xff + } + return out +} + +function stats(samples: number[]): LatencyStats { + if (!samples.length) return { p50_ms: 0, p95_ms: 0, p99_ms: 0, max_ms: 0, mean_ms: 0 } + const s = [...samples].sort((a, b) => a - b) + const q = (p: number) => s[Math.min(s.length - 1, Math.floor(p * (s.length - 1)))] + return { + p50_ms: q(0.5), p95_ms: q(0.95), p99_ms: q(0.99), max_ms: s[s.length - 1], + mean_ms: samples.reduce((a, b) => a + b, 0) / samples.length, + } +} + +function opResult(op: OpResult['op'], lat: number[], failures: number, bytesEach: number): OpResult { + const totalMs = lat.reduce((a, b) => a + b, 0) || 1 + return { + op, + count: lat.length, + failures, + latency: stats(lat), + throughput_ops_s: (lat.length / totalMs) * 1000, + throughput_mb_s: ((lat.length * bytesEach) / (1024 * 1024) / totalMs) * 1000, + } +} + +/** + * Benchmark an ObjectBackend over `n_ops` content-addressed blobs: put, then get, then verify + * (re-hash the retrieved bytes). Deterministic given `seed`. Fail-closed on any integrity mismatch. + */ +export async function benchObjectBackend( + backend: ObjectBackend, + config: StorageBenchConfig, +): Promise { + const seed = config.seed ?? 0x5010 + const started_at = new Date().toISOString() + const hashes: string[] = [] + const putLat: number[] = [] + const getLat: number[] = [] + const verLat: number[] = [] + let putFail = 0, getFail = 0 + let integrity_verified = 0, integrity_failures = 0 + + for (let i = 0; i < config.n_ops; i++) { + const bytes = seededPayload(seed, i, config.payload_bytes) + const hash = sha256(bytes) + hashes.push(hash) + const t0 = performance.now() + try { await backend.put(hash, bytes) } catch { putFail++ } + putLat.push(performance.now() - t0) + } + + for (let i = 0; i < hashes.length; i++) { + const expected = seededPayload(seed, i, config.payload_bytes) + const t0 = performance.now() + let got: Buffer | undefined + try { got = await backend.get(hashes[i]!) } catch { got = undefined } + getLat.push(performance.now() - t0) + if (!got) { getFail++; continue } + + // verify: bytes round-trip AND the content hash re-derives (content-addressing holds). + const tv = performance.now() + const rehash = sha256(got) + verLat.push(performance.now() - tv) + if (rehash === hashes[i] && got.equals(expected)) integrity_verified++ + else integrity_failures++ + } + + const opFailures = putFail + getFail + const finished_at = new Date().toISOString() + return { + backend_id: config.backend_id, + config: { ...config, seed }, + ops: [ + opResult('put', putLat, putFail, config.payload_bytes), + opResult('get', getLat, getFail, config.payload_bytes), + opResult('verify', verLat, 0, config.payload_bytes), + ], + integrity_verified, + integrity_failures, + result: integrity_failures === 0 && opFailures === 0 && integrity_verified === config.n_ops ? 'pass' : 'fail', + started_at, + finished_at, + } +} + +// ── SocioProphet storage-standard EvaluationRecord (evaluation-record-standard.v1) ────────────── + +export interface EvaluationRecord { + id: string + evaluation_track_ref: string + subject_ref: string + subject_type: 'platform_capability' + attempt_refs: { id: string; attempt_mode: 'benchmark_run'; measurements: StorageBenchResult }[] + metric_refs: string[] + result_summary: string + result: 'pass' | 'pass_with_findings' | 'remediation_required' | 'fail' | 'blocked' | 'unknown' + evidence_bundle_ref: string + review_state: 'draft' + created_at: string + updated_at: string +} + +/** Wrap a bench result as a storage-standard EvaluationRecord (evidence-first, regression-checkable). */ +export function toEvaluationRecord( + r: StorageBenchResult, + opts: { subject_ref: string; evaluation_track_ref: string }, +): EvaluationRecord { + const put = r.ops.find((o) => o.op === 'put')! + const get = r.ops.find((o) => o.op === 'get')! + const now = new Date().toISOString() + return { + id: `evalrec:storage-bench:${r.backend_id}:${r.started_at}`, + evaluation_track_ref: opts.evaluation_track_ref, + subject_ref: opts.subject_ref, + subject_type: 'platform_capability', + attempt_refs: [{ id: `attempt:${r.backend_id}:${r.started_at}`, attempt_mode: 'benchmark_run', measurements: r }], + metric_refs: ['put.p95_ms', 'get.p95_ms', 'put.throughput_mb_s', 'integrity_verified'], + result_summary: + `${r.backend_id}: ${r.config.n_ops}×${r.config.payload_bytes}B — ` + + `put p95 ${put.latency.p95_ms.toFixed(3)}ms, get p95 ${get.latency.p95_ms.toFixed(3)}ms, ` + + `integrity ${r.integrity_verified}/${r.config.n_ops}`, + result: r.result === 'pass' ? 'pass' : 'fail', + evidence_bundle_ref: `evidence:storage-bench:${r.backend_id}:${r.started_at}`, + review_state: 'draft', + created_at: r.started_at, + updated_at: now, + } +}