From 12913cbc4486c3d7797c920c99b172228b1abd7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 05:06:28 +0000 Subject: [PATCH 01/17] Bump @opentelemetry/semantic-conventions from 1.40.0 to 1.41.1 Bumps [@opentelemetry/semantic-conventions](https://github.com/open-telemetry/opentelemetry-js) from 1.40.0 to 1.41.1. - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/semconv/v1.40.0...semconv/v1.41.1) --- updated-dependencies: - dependency-name: "@opentelemetry/semantic-conventions" dependency-version: 1.41.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- yarn.lock | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..9d5c22b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -1910,9 +1910,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", - "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", "license": "Apache-2.0", "engines": { "node": ">=14" diff --git a/yarn.lock b/yarn.lock index f957f93..a74d09f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1054,9 +1054,9 @@ "@opentelemetry/sdk-trace-base" "2.7.1" "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.40.0": - version "1.40.0" - resolved "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz" - integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw== + version "1.41.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz#b04e7151c5913a7a006d4f465479da75efb98a7a" + integrity sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA== "@pkgjs/parseargs@^0.11.0": version "0.11.0" From 140127874208519571a2c591ccad3d03bc571217 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 05:06:39 +0000 Subject: [PATCH 02/17] Bump eslint from 10.3.0 to 10.4.0 Bumps [eslint](https://github.com/eslint/eslint) from 10.3.0 to 10.4.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.4.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.4.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++--------- yarn.lock | 16 ++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..3560b2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -690,9 +690,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3648,16 +3648,16 @@ } }, "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", diff --git a/yarn.lock b/yarn.lock index f957f93..fbde843 100644 --- a/yarn.lock +++ b/yarn.lock @@ -313,10 +313,10 @@ debug "^4.3.1" minimatch "^10.2.4" -"@eslint/config-helpers@^0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.5.5.tgz#ae16134e4792ac5fbdc533548a24ac1ea9f7f3ae" - integrity sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w== +"@eslint/config-helpers@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz#ef9a36881d39dfd5dbeac22b0da997fabfb08b03" + integrity sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA== dependencies: "@eslint/core" "^1.2.1" @@ -1993,14 +1993,14 @@ eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1: integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== eslint@^10.0.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.3.0.tgz#ed5b810eb8e0191bf24bddcf9cdb45b974e0a16d" - integrity sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw== + version "10.4.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.4.0.tgz#d86b6c405de0f19f3318c47139b8cb6771b3f592" + integrity sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.2" "@eslint/config-array" "^0.23.5" - "@eslint/config-helpers" "^0.5.5" + "@eslint/config-helpers" "^0.6.0" "@eslint/core" "^1.2.1" "@eslint/plugin-kit" "^0.7.1" "@humanfs/node" "^0.16.6" From a64369cb8acb18771f203d7a9748161a172ba124 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 12:46:16 +0000 Subject: [PATCH 03/17] Bump qs from 6.14.2 to 6.15.2 Bumps [qs](https://github.com/ljharb/qs) from 6.14.2 to 6.15.2. - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.14.2...v6.15.2) --- updated-dependencies: - dependency-name: qs dependency-version: 6.15.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- yarn.lock | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..cd79467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -6456,9 +6456,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/yarn.lock b/yarn.lock index f957f93..ae1c482 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3619,9 +3619,9 @@ pure-rand@^7.0.0: integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== qs@^6.14.0, qs@^6.14.1: - version "6.14.2" - resolved "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz" - integrity sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q== + version "6.15.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.2.tgz#fd55426d710403ddccc45e0f9eab16db7727ece9" + integrity sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw== dependencies: side-channel "^1.1.0" From 1c2d4d1d4be8bac482c6ad970fce1edfedc6dc1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 04:57:07 +0000 Subject: [PATCH 04/17] Bump ts-jest from 29.4.9 to 29.4.11 Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.4.9 to 29.4.11. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.9...v29.4.11) --- updated-dependencies: - dependency-name: ts-jest dependency-version: 29.4.11 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++--------- yarn.lock | 16 ++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..3287010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -6674,9 +6674,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -7306,9 +7306,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.9", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", - "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", "dev": true, "license": "MIT", "dependencies": { @@ -7318,7 +7318,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.4", + "semver": "^7.8.0", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, diff --git a/yarn.lock b/yarn.lock index f957f93..789a593 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3744,10 +3744,10 @@ semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4: - version "7.7.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" - integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +semver@^7.5.3, semver@^7.5.4, semver@^7.7.2, semver@^7.7.3, semver@^7.8.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.1.tgz#bf4970b5e70fda0686363cc18bfe8805d5ed957e" + integrity sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg== send@^1.1.0, send@^1.2.0: version "1.2.1" @@ -4055,9 +4055,9 @@ ts-api-utils@^2.5.0: integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== ts-jest@^29.4.5: - version "29.4.9" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.9.tgz#47dc33d0f5c36bddcedd16afefae285e0b049d2d" - integrity sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ== + version "29.4.11" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.11.tgz#42f5de21c37ccc01a580253afae6955abbf4d0b3" + integrity sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g== dependencies: bs-logger "^0.2.6" fast-json-stable-stringify "^2.1.0" @@ -4065,7 +4065,7 @@ ts-jest@^29.4.5: json5 "^2.2.3" lodash.memoize "^4.1.2" make-error "^1.3.6" - semver "^7.7.4" + semver "^7.8.0" type-fest "^4.41.0" yargs-parser "^21.1.1" From b5326264c0b7d89534430cee627fc8ba4205c4be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 04:57:20 +0000 Subject: [PATCH 05/17] Bump @types/node from 25.6.2 to 25.9.1 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.2 to 25.9.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++--------- yarn.lock | 16 ++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..c699916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -2149,12 +2149,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/stack-utils": { @@ -7469,9 +7469,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unpipe": { diff --git a/yarn.lock b/yarn.lock index f957f93..26d5554 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1223,11 +1223,11 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@*", "@types/node@>=13.7.0", "@types/node@^25": - version "25.6.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.2.tgz#8c491201373690e4ef2a2ffed0dfb510a5830b92" - integrity sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw== + version "25.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b" + integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg== dependencies: - undici-types "~7.19.0" + undici-types ">=7.24.0 <7.24.7" "@types/stack-utils@^2.0.3": version "2.0.3" @@ -4125,10 +4125,10 @@ uglify-js@^3.1.4: resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== -undici-types@~7.19.0: - version "7.19.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" - integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== +"undici-types@>=7.24.0 <7.24.7": + version "7.24.6" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" + integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== unpipe@~1.0.0: version "1.0.0" From a2a78b12ab841474638efe90f128dfa5c0637416 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 04:57:34 +0000 Subject: [PATCH 06/17] Bump typescript-eslint from 8.59.2 to 8.59.4 Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.2 to 8.59.4. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.59.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 126 ++++++++++++++++++++-------------------- yarn.lock | 142 +++++++++++++++++++++++----------------------- 2 files changed, 134 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..5fffb6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -2182,17 +2182,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2205,7 +2205,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", + "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2221,16 +2221,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "engines": { @@ -2246,14 +2246,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "engines": { @@ -2268,14 +2268,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2286,9 +2286,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", "dev": true, "license": "MIT", "engines": { @@ -2303,15 +2303,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2328,9 +2328,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", "dev": true, "license": "MIT", "engines": { @@ -2342,16 +2342,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2409,16 +2409,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2433,13 +2433,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -7431,16 +7431,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/yarn.lock b/yarn.lock index f957f93..189ee24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1246,100 +1246,100 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz#f37b2c189a0177141fe3de3b08f2a83991bfdbfa" - integrity sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ== +"@typescript-eslint/eslint-plugin@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz#c67bfee32caae9cb587dce1ac59c3bf43b659707" + integrity sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.59.2" - "@typescript-eslint/type-utils" "8.59.2" - "@typescript-eslint/utils" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/scope-manager" "8.59.4" + "@typescript-eslint/type-utils" "8.59.4" + "@typescript-eslint/utils" "8.59.4" + "@typescript-eslint/visitor-keys" "8.59.4" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.2.tgz#e2fd0084baa5dd0c24cd789af1c72cbc3a7a1c62" - integrity sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ== +"@typescript-eslint/parser@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.4.tgz#77d99e3b27663e7a22cf12c3fb769db509e5e93c" + integrity sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ== dependencies: - "@typescript-eslint/scope-manager" "8.59.2" - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/scope-manager" "8.59.4" + "@typescript-eslint/types" "8.59.4" + "@typescript-eslint/typescript-estree" "8.59.4" + "@typescript-eslint/visitor-keys" "8.59.4" debug "^4.4.3" -"@typescript-eslint/project-service@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz#f8b8cbf8692e3a51c2c394acf8cf6900f7e755af" - integrity sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw== +"@typescript-eslint/project-service@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz#5830535a0e7a3ae806e2669964f47a74c4bc6b0e" + integrity sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.59.2" - "@typescript-eslint/types" "^8.59.2" + "@typescript-eslint/tsconfig-utils" "^8.59.4" + "@typescript-eslint/types" "^8.59.4" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz#63cbd0af2e3180949d6be81122cc555bc71e736d" - integrity sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg== +"@typescript-eslint/scope-manager@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz#507d1258c758147dac1adee9517a205a8ac1e046" + integrity sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q== dependencies: - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/types" "8.59.4" + "@typescript-eslint/visitor-keys" "8.59.4" -"@typescript-eslint/tsconfig-utils@8.59.2", "@typescript-eslint/tsconfig-utils@^8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz#6e92bc412083753185a79c9f1431e78169d9232f" - integrity sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw== +"@typescript-eslint/tsconfig-utils@8.59.4", "@typescript-eslint/tsconfig-utils@^8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz#218ba229d96dde35212e3a76a7d0a6bc831398be" + integrity sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA== -"@typescript-eslint/type-utils@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz#a60a1192a804fa472a92c41656853ac6a9ba7176" - integrity sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ== +"@typescript-eslint/type-utils@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz#359fc53ba39a1f1860fddda40ebe5bfe0d87faed" + integrity sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA== dependencies: - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" - "@typescript-eslint/utils" "8.59.2" + "@typescript-eslint/types" "8.59.4" + "@typescript-eslint/typescript-estree" "8.59.4" + "@typescript-eslint/utils" "8.59.4" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.59.2", "@typescript-eslint/types@^8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.2.tgz#01caabcd7e4715c33ad5e11cab260829714d6b9c" - integrity sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q== +"@typescript-eslint/types@8.59.4", "@typescript-eslint/types@^8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.4.tgz#c29d5c21bfbaa8347ddc677d3ac1fcd2db0f848e" + integrity sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q== -"@typescript-eslint/typescript-estree@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz#6a217ef65b18dbd12c718fc86a675d1d7a1414cc" - integrity sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg== +"@typescript-eslint/typescript-estree@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz#d005e5e1fb425526f39685594bed34a04ad755ea" + integrity sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag== dependencies: - "@typescript-eslint/project-service" "8.59.2" - "@typescript-eslint/tsconfig-utils" "8.59.2" - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/visitor-keys" "8.59.2" + "@typescript-eslint/project-service" "8.59.4" + "@typescript-eslint/tsconfig-utils" "8.59.4" + "@typescript-eslint/types" "8.59.4" + "@typescript-eslint/visitor-keys" "8.59.4" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.2.tgz#ff619a6a3075f4017fa91b8610b752a8ca3366aa" - integrity sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q== +"@typescript-eslint/utils@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.4.tgz#8ccd2b08aecc72c7efc0d7ac6695631d199d256e" + integrity sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.59.2" - "@typescript-eslint/types" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" + "@typescript-eslint/scope-manager" "8.59.4" + "@typescript-eslint/types" "8.59.4" + "@typescript-eslint/typescript-estree" "8.59.4" -"@typescript-eslint/visitor-keys@8.59.2": - version "8.59.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz#5ccc486913cd347883d69158836b1189a660bfe6" - integrity sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA== +"@typescript-eslint/visitor-keys@8.59.4": + version "8.59.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz#1ac23b747b011f5cbdb449da97769f6c5f3a9355" + integrity sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ== dependencies: - "@typescript-eslint/types" "8.59.2" + "@typescript-eslint/types" "8.59.4" eslint-visitor-keys "^5.0.0" "@ungap/structured-clone@^1.3.0": @@ -4106,14 +4106,14 @@ type-is@^2.0.1: mime-types "^3.0.0" typescript-eslint@^8.55.0: - version "8.59.2" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz#e24b4f7232e20112e40572dba162a829a738ce98" - integrity sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ== - dependencies: - "@typescript-eslint/eslint-plugin" "8.59.2" - "@typescript-eslint/parser" "8.59.2" - "@typescript-eslint/typescript-estree" "8.59.2" - "@typescript-eslint/utils" "8.59.2" + version "8.59.4" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.4.tgz#834e3b53f4d1a764a985ceb8592c4a95d6a8da7c" + integrity sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ== + dependencies: + "@typescript-eslint/eslint-plugin" "8.59.4" + "@typescript-eslint/parser" "8.59.4" + "@typescript-eslint/typescript-estree" "8.59.4" + "@typescript-eslint/utils" "8.59.4" typescript@^6.0.3: version "6.0.3" From ed278226e3c56d82f33d9f3e17b338a585ab516c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:32:56 +0000 Subject: [PATCH 07/17] Bump protobufjs from 7.5.6 to 7.6.1 Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.6 to 7.6.1. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.1/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.6...protobufjs-v7.6.1) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 7.6.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 73 +++++++++++++++++++++++------------------------ yarn.lock | 41 +++++++++++++------------- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..b01337b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -770,30 +770,6 @@ "node": ">=6" } }, - "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/@hono/node-server": { "version": "1.19.13", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", @@ -1961,19 +1937,18 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -1983,9 +1958,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { @@ -6404,6 +6379,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/protobufjs": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", + "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/yarn.lock b/yarn.lock index f957f93..44e6ebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1083,28 +1083,27 @@ resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz" integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g== -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== +"@protobufjs/eventemitter@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz#d512cb26c0ae026091ee2c1167f1be6faf5c842a" + integrity sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg== -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== +"@protobufjs/fetch@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.1.tgz#4d6fc00c8fb64016a5c81b469d549046350f1065" + integrity sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw== dependencies: "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" "@protobufjs/float@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== -"@protobufjs/inquire@^1.1.0", "@protobufjs/inquire@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz" - integrity sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew== +"@protobufjs/inquire@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.2.tgz#ae64fbc014ff44c8bfad03dd4c93cd2d6a4c82db" + integrity sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw== "@protobufjs/path@^1.1.2": version "1.1.2" @@ -3198,7 +3197,7 @@ lodash.memoize@^4.1.2: resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== -long@^5.0.0: +long@^5.0.0, long@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== @@ -3575,22 +3574,22 @@ pretty-format@30.4.1: react-is-19 "npm:react-is@^19.2.5" protobufjs@^7.5.3: - version "7.5.6" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz" - integrity sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg== + version "7.6.1" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.6.1.tgz#6320bb08c3be7dcfc6f9193ee03d3a4643f1eb37" + integrity sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" "@protobufjs/codegen" "^2.0.5" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" + "@protobufjs/eventemitter" "^1.1.1" + "@protobufjs/fetch" "^1.1.1" "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.1" + "@protobufjs/inquire" "^1.1.2" "@protobufjs/path" "^1.1.2" "@protobufjs/pool" "^1.1.0" "@protobufjs/utf8" "^1.1.1" "@types/node" ">=13.7.0" - long "^5.0.0" + long "^5.3.2" proxy-addr@^2.0.7: version "2.0.7" From 01e9a9017fdc57f6c53ed7bbe3f495271ac6f60e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:48:03 +0000 Subject: [PATCH 08/17] Bump @grpc/grpc-js from 1.14.3 to 1.14.4 Bumps [@grpc/grpc-js](https://github.com/grpc/grpc-node) from 1.14.3 to 1.14.4. - [Release notes](https://github.com/grpc/grpc-node/releases) - [Commits](https://github.com/grpc/grpc-node/compare/@grpc/grpc-js@1.14.3...@grpc/grpc-js@1.14.4) --- updated-dependencies: - dependency-name: "@grpc/grpc-js" dependency-version: 1.14.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- yarn.lock | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..62c96b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -740,9 +740,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.8.0", diff --git a/yarn.lock b/yarn.lock index f957f93..4815230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -341,9 +341,9 @@ levn "^0.4.1" "@grpc/grpc-js@^1.14.3": - version "1.14.3" - resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz" - integrity sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA== + version "1.14.4" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.4.tgz#e73ff57d97802f063999545f43ebb2b1eca65d9d" + integrity sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ== dependencies: "@grpc/proto-loader" "^0.8.0" "@js-sdsl/ordered-map" "^4.4.2" From 6cb89a6948e133484f572a175a25e7f85b059830 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 19:34:55 +0000 Subject: [PATCH 09/17] Bump @opentelemetry/core, @opentelemetry/exporter-metrics-otlp-grpc, @opentelemetry/exporter-trace-otlp-grpc, @opentelemetry/resources, @opentelemetry/sdk-metrics and @opentelemetry/sdk-node Bumps [@opentelemetry/core](https://github.com/open-telemetry/opentelemetry-js) to 2.8.0 and updates ancestor dependencies [@opentelemetry/core](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-metrics-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/exporter-trace-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/resources](https://github.com/open-telemetry/opentelemetry-js), [@opentelemetry/sdk-metrics](https://github.com/open-telemetry/opentelemetry-js) and [@opentelemetry/sdk-node](https://github.com/open-telemetry/opentelemetry-js). These dependencies need to be updated together. Updates `@opentelemetry/core` from 2.7.1 to 2.8.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/v2.7.1...v2.8.0) Updates `@opentelemetry/exporter-metrics-otlp-grpc` from 0.218.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.218.0...experimental/v0.219.0) Updates `@opentelemetry/exporter-trace-otlp-grpc` from 0.218.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.218.0...experimental/v0.219.0) Updates `@opentelemetry/resources` from 2.7.1 to 2.8.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/v2.7.1...v2.8.0) Updates `@opentelemetry/sdk-metrics` from 2.7.1 to 2.8.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/v2.7.1...v2.8.0) Updates `@opentelemetry/sdk-node` from 0.218.0 to 0.219.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.218.0...experimental/v0.219.0) --- updated-dependencies: - dependency-name: "@opentelemetry/core" dependency-version: 2.8.0 dependency-type: indirect - dependency-name: "@opentelemetry/exporter-metrics-otlp-grpc" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/exporter-trace-otlp-grpc" dependency-version: 0.219.0 dependency-type: direct:production - dependency-name: "@opentelemetry/resources" dependency-version: 2.8.0 dependency-type: direct:production - dependency-name: "@opentelemetry/sdk-metrics" dependency-version: 2.8.0 dependency-type: direct:production - dependency-name: "@opentelemetry/sdk-node" dependency-version: 0.219.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 393 ++++++++++++++++++------------------- package.json | 6 +- yarn.lock | 487 +++++++++++++++++++++++----------------------- 3 files changed, 444 insertions(+), 442 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..a745875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.218.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.218.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", "@opentelemetry/resources": "^2.7.0", "@opentelemetry/sdk-metrics": "^2.7.0", - "@opentelemetry/sdk-node": "^0.218.0", + "@opentelemetry/sdk-node": "^0.219.0", "@opentelemetry/semantic-conventions": "^1.40.0" }, "bin": { @@ -1415,9 +1415,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1427,12 +1427,12 @@ } }, "node_modules/@opentelemetry/configuration": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.218.0.tgz", - "integrity": "sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.219.0.tgz", + "integrity": "sha512-wXZUYv4ngu43nA4WEhuXNacm46LW+17LRM8nKyIhBzroRA24PBYjMnakwzR/w777nFUB5xlgsYTTeuXxumZM1Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.8.0", "yaml": "^2.0.0" }, "engines": { @@ -1443,9 +1443,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", - "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz", + "integrity": "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1455,9 +1455,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", + "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1470,17 +1470,17 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/sdk-logs": "0.218.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/sdk-logs": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1490,16 +1490,16 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz", - "integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.219.0.tgz", + "integrity": "sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/sdk-logs": "0.218.0" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/sdk-logs": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1509,18 +1509,18 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz", - "integrity": "sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.219.0.tgz", + "integrity": "sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1530,19 +1530,19 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1552,16 +1552,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz", - "integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz", + "integrity": "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1571,17 +1571,17 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz", - "integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz", + "integrity": "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1591,14 +1591,14 @@ } }, "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz", - "integrity": "sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.219.0.tgz", + "integrity": "sha512-TxOnJ85eWJY5JyOJsNMXiRTYlkDcOv0u3KbXEzWCc+tUS9sjL/BC6BcdxZ0B9r2OFVqsrZFXUzSD2sZUy42Ucw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1609,18 +1609,18 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz", + "integrity": "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1630,16 +1630,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", - "integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz", + "integrity": "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1649,16 +1649,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", - "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz", + "integrity": "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1668,14 +1668,14 @@ } }, "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz", - "integrity": "sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.8.0.tgz", + "integrity": "sha512-Mj84UkEa17BK2o903VTXW3wM8CrSZexGs4tRGVZVIMM9ni1T6TuGx5IrRfoWKAbshx42D5/kc7YV+axypLPYyA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1686,12 +1686,12 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", - "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", + "integrity": "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/api-logs": "0.219.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, @@ -1703,13 +1703,13 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", + "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-transformer": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1719,15 +1719,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz", - "integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz", + "integrity": "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-transformer": "0.219.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1737,17 +1737,17 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", + "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1757,12 +1757,12 @@ } }, "node_modules/@opentelemetry/propagator-b3": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz", - "integrity": "sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.8.0.tgz", + "integrity": "sha512-SazlvuSKi5533rPHTW2TwBwdMakhjZST4SYs0YauuvfGDkT13KbG1gJS75hV0uWVeevhtVP9sAIlaZLTHdSbMg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1" + "@opentelemetry/core": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1772,12 +1772,12 @@ } }, "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz", - "integrity": "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.8.0.tgz", + "integrity": "sha512-Xnz9zZvvQzUw+9DrOn0MomR7BxFCkA2pcfXBQuHC28ndJpSbjLs7knzYb05kw5SyCjSsEWombkZMgGcJSk8JVg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1" + "@opentelemetry/core": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1787,12 +1787,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", + "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1803,14 +1803,14 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", + "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1821,13 +1821,13 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", + "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1837,35 +1837,36 @@ } }, "node_modules/@opentelemetry/sdk-node": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz", - "integrity": "sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==", + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.219.0.tgz", + "integrity": "sha512-NWLpWLEb8gV3+JBHYoIrktbM385wyHpRJoh3J/4Q52d4PR+AlPMNGJT3DzBUrDSUEVbKAXoHR+EDAPxtiNcj8g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/configuration": "0.218.0", - "@opentelemetry/context-async-hooks": "2.7.1", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-logs-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-logs-otlp-http": "0.218.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.218.0", - "@opentelemetry/exporter-prometheus": "0.218.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-trace-otlp-http": "0.218.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.218.0", - "@opentelemetry/exporter-zipkin": "2.7.1", - "@opentelemetry/instrumentation": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/propagator-b3": "2.7.1", - "@opentelemetry/propagator-jaeger": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1", - "@opentelemetry/sdk-trace-node": "2.7.1", + "@opentelemetry/api-logs": "0.219.0", + "@opentelemetry/configuration": "0.219.0", + "@opentelemetry/context-async-hooks": "2.8.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.219.0", + "@opentelemetry/exporter-logs-otlp-http": "0.219.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.219.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.219.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.219.0", + "@opentelemetry/exporter-prometheus": "0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.219.0", + "@opentelemetry/exporter-trace-otlp-http": "0.219.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.219.0", + "@opentelemetry/exporter-zipkin": "2.8.0", + "@opentelemetry/instrumentation": "0.219.0", + "@opentelemetry/otlp-exporter-base": "0.219.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", + "@opentelemetry/propagator-b3": "2.8.0", + "@opentelemetry/propagator-jaeger": "2.8.0", + "@opentelemetry/resources": "2.8.0", + "@opentelemetry/sdk-logs": "0.219.0", + "@opentelemetry/sdk-metrics": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0", + "@opentelemetry/sdk-trace-node": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1876,13 +1877,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", + "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1893,14 +1894,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", - "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz", + "integrity": "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.7.1", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/context-async-hooks": "2.8.0", + "@opentelemetry/core": "2.8.0", + "@opentelemetry/sdk-trace-base": "2.8.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4695,9 +4696,9 @@ } }, "node_modules/import-in-the-middle": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", - "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.1.0.tgz", + "integrity": "sha512-c0AeAV8VcwZzfYE7euTZY3H+VXUPMVugiovdosq80lqEXJmOekg3zGUAYg6KImHMaMuBoTUfTv7xNpUFdy0hJA==", "license": "Apache-2.0", "dependencies": { "acorn": "^8.15.0", diff --git a/package.json b/package.json index 79c2201..da5d5c1 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,11 @@ "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.218.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.218.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", "@opentelemetry/resources": "^2.7.0", "@opentelemetry/sdk-metrics": "^2.7.0", - "@opentelemetry/sdk-node": "^0.218.0", + "@opentelemetry/sdk-node": "^0.219.0", "@opentelemetry/semantic-conventions": "^1.40.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index f957f93..98247f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -765,10 +765,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@opentelemetry/api-logs@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz#7b9818e8dfdf1d3dcab88bfe4d6724f2f831f7ec" - integrity sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw== +"@opentelemetry/api-logs@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz#3303f10f43e6ff1741f8f6019e43a92723781630" + integrity sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg== dependencies: "@opentelemetry/api" "^1.3.0" @@ -777,281 +777,282 @@ resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz" integrity sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q== -"@opentelemetry/configuration@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/configuration/-/configuration-0.218.0.tgz#a5fdd50ec9cfa0adb3bb41202cb39f79c5f4e7d9" - integrity sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ== +"@opentelemetry/configuration@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/configuration/-/configuration-0.219.0.tgz#cb950aaa77bdb921a3fd636dc792f8beb712be17" + integrity sha512-wXZUYv4ngu43nA4WEhuXNacm46LW+17LRM8nKyIhBzroRA24PBYjMnakwzR/w777nFUB5xlgsYTTeuXxumZM1Q== dependencies: - "@opentelemetry/core" "2.7.1" + "@opentelemetry/core" "2.8.0" yaml "^2.0.0" -"@opentelemetry/context-async-hooks@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz#1555a6fb269596416d8c626fd020c3f2c38e071f" - integrity sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ== +"@opentelemetry/context-async-hooks@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.8.0.tgz#24381ef388bc28d53fa8dad72dfd1429acc82016" + integrity sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw== -"@opentelemetry/core@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.7.1.tgz#162bfab46d6ff4da1bef240ea52e23a926b0fdbc" - integrity sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw== +"@opentelemetry/core@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.8.0.tgz#f6e86de3688bdb54a6ca8f4935363a5b588ae91c" + integrity sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww== dependencies: "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/exporter-logs-otlp-grpc@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.218.0.tgz#7e5a7e624074d6449590c851d58efe60096314b3" - integrity sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g== +"@opentelemetry/exporter-logs-otlp-grpc@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.219.0.tgz#d828e7495d05c54fd51a6495094eaf9e485805c1" + integrity sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ== dependencies: "@grpc/grpc-js" "^1.14.3" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-grpc-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/sdk-logs" "0.218.0" - -"@opentelemetry/exporter-logs-otlp-http@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz#0996cb45e0ebc6c7465445430a018edcac9871b4" - integrity sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw== - dependencies: - "@opentelemetry/api-logs" "0.218.0" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/sdk-logs" "0.218.0" - -"@opentelemetry/exporter-logs-otlp-proto@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz#e539720b2e145e85a7a4901a1eabb0b2e77990f8" - integrity sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg== - dependencies: - "@opentelemetry/api-logs" "0.218.0" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-logs" "0.218.0" - "@opentelemetry/sdk-trace-base" "2.7.1" - -"@opentelemetry/exporter-metrics-otlp-grpc@0.218.0", "@opentelemetry/exporter-metrics-otlp-grpc@^0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz#6d66c2638702489d063b8857741eee9cea1d21d6" - integrity sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ== + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/sdk-logs" "0.219.0" + +"@opentelemetry/exporter-logs-otlp-http@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.219.0.tgz#3a4764ec408efdf9c573cfc3e4ffc41392694d3b" + integrity sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A== + dependencies: + "@opentelemetry/api-logs" "0.219.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/sdk-logs" "0.219.0" + +"@opentelemetry/exporter-logs-otlp-proto@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.219.0.tgz#de0431c17a944928471ffab1dcea9e02e70271ac" + integrity sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg== + dependencies: + "@opentelemetry/api-logs" "0.219.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-logs" "0.219.0" + "@opentelemetry/sdk-trace-base" "2.8.0" + +"@opentelemetry/exporter-metrics-otlp-grpc@0.219.0", "@opentelemetry/exporter-metrics-otlp-grpc@^0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz#b9d1ede26f45adfbcbf087dd7d4065dfc1268121" + integrity sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ== dependencies: "@grpc/grpc-js" "^1.14.3" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/exporter-metrics-otlp-http" "0.218.0" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-grpc-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-metrics" "2.7.1" - -"@opentelemetry/exporter-metrics-otlp-http@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz#c205581585d04c5106d1ca766e23632006e46a2c" - integrity sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-metrics" "2.7.1" - -"@opentelemetry/exporter-metrics-otlp-proto@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz#66642b8bb5e654ca7a5944a4f0b490814fc875fd" - integrity sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/exporter-metrics-otlp-http" "0.218.0" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-metrics" "2.7.1" - -"@opentelemetry/exporter-prometheus@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz#41bddc97d34cd9d9993382b9c10fed19a8de63f4" - integrity sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-metrics" "2.7.1" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/exporter-metrics-otlp-http" "0.219.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-metrics" "2.8.0" + +"@opentelemetry/exporter-metrics-otlp-http@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz#aed2238678486434cff020cbdfef07b24f1370b6" + integrity sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-metrics" "2.8.0" + +"@opentelemetry/exporter-metrics-otlp-proto@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz#7a7a20dba913f00c115fb48901502680c41e23dc" + integrity sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/exporter-metrics-otlp-http" "0.219.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-metrics" "2.8.0" + +"@opentelemetry/exporter-prometheus@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.219.0.tgz#8ea911269156f3cd4f08a1e8e5727df3f316b687" + integrity sha512-TxOnJ85eWJY5JyOJsNMXiRTYlkDcOv0u3KbXEzWCc+tUS9sjL/BC6BcdxZ0B9r2OFVqsrZFXUzSD2sZUy42Ucw== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-metrics" "2.8.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/exporter-trace-otlp-grpc@0.218.0", "@opentelemetry/exporter-trace-otlp-grpc@^0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz#5d5532ab94d5514bec8aedc19ce3170b6381eed1" - integrity sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw== +"@opentelemetry/exporter-trace-otlp-grpc@0.219.0", "@opentelemetry/exporter-trace-otlp-grpc@^0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz#ef421636a3e360b4e8e1d26a2cae0d6f49c5187a" + integrity sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q== dependencies: "@grpc/grpc-js" "^1.14.3" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-grpc-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" - -"@opentelemetry/exporter-trace-otlp-http@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz#36d6abf6d639b9ea861603c61f434751c0b1a0ea" - integrity sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" - -"@opentelemetry/exporter-trace-otlp-proto@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz#de0b2b3545149dafedd2b948fb891f9bf962940c" - integrity sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" - -"@opentelemetry/exporter-zipkin@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz#3b79d223adc8c097ba3323e4de4ed8abb83c789e" - integrity sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" + +"@opentelemetry/exporter-trace-otlp-http@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.219.0.tgz#37ad13bd871fe5d983754bb2ad2e2ac2f3e167ec" + integrity sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" + +"@opentelemetry/exporter-trace-otlp-proto@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz#cec3c1da46bb3ac5774461830634d6509b797bf1" + integrity sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" + +"@opentelemetry/exporter-zipkin@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.8.0.tgz#03f5ca641f611fc15b9530d2a2ec6238a167bda1" + integrity sha512-Mj84UkEa17BK2o903VTXW3wM8CrSZexGs4tRGVZVIMM9ni1T6TuGx5IrRfoWKAbshx42D5/kc7YV+axypLPYyA== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/instrumentation@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz#fcceb4ffb45f99c0d292600769150fc5944dc3e9" - integrity sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg== +"@opentelemetry/instrumentation@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz#84399affd8ee4a12cb199f325c4858654d8dedee" + integrity sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ== dependencies: - "@opentelemetry/api-logs" "0.218.0" + "@opentelemetry/api-logs" "0.219.0" import-in-the-middle "^3.0.0" require-in-the-middle "^8.0.0" -"@opentelemetry/otlp-exporter-base@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz#f33e3217ce568756baa132fabe30f0c9345db086" - integrity sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA== +"@opentelemetry/otlp-exporter-base@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz#68f4908f45f6df577d8e1c5b42761645f01cffc5" + integrity sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA== dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-transformer" "0.218.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-transformer" "0.219.0" -"@opentelemetry/otlp-grpc-exporter-base@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz#dc5a1fec716245d84dc4167de9b1c607e07fb6a7" - integrity sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A== +"@opentelemetry/otlp-grpc-exporter-base@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz#626713dc38a8879cbc95304832971e6daa889248" + integrity sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw== dependencies: "@grpc/grpc-js" "^1.14.3" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/otlp-transformer" "0.218.0" - -"@opentelemetry/otlp-transformer@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz#6784d0ddd13803c63a1b24072606c02fc21b9071" - integrity sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ== - dependencies: - "@opentelemetry/api-logs" "0.218.0" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-logs" "0.218.0" - "@opentelemetry/sdk-metrics" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" - -"@opentelemetry/propagator-b3@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz#107fe3e16d0728c489edbad221c402ee197514a4" - integrity sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A== - dependencies: - "@opentelemetry/core" "2.7.1" - -"@opentelemetry/propagator-jaeger@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz#e8ebf3f6c0e9aa525cf041893425889cf3e69125" - integrity sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q== - dependencies: - "@opentelemetry/core" "2.7.1" - -"@opentelemetry/resources@2.7.1", "@opentelemetry/resources@^2.7.0": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.7.1.tgz#3b2a9179f6119bb1f2cddefe41ba9b2855504a5d" - integrity sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ== - dependencies: - "@opentelemetry/core" "2.7.1" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-transformer" "0.219.0" + +"@opentelemetry/otlp-transformer@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz#86305b29f564220666f6e410b7eded57bb5e6c3a" + integrity sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ== + dependencies: + "@opentelemetry/api-logs" "0.219.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-logs" "0.219.0" + "@opentelemetry/sdk-metrics" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" + +"@opentelemetry/propagator-b3@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-2.8.0.tgz#d8dadd951831cc96bc2e44eadf04d2b3d1a9b320" + integrity sha512-SazlvuSKi5533rPHTW2TwBwdMakhjZST4SYs0YauuvfGDkT13KbG1gJS75hV0uWVeevhtVP9sAIlaZLTHdSbMg== + dependencies: + "@opentelemetry/core" "2.8.0" + +"@opentelemetry/propagator-jaeger@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.8.0.tgz#b8866476fd4a3953dd660a6ab5dc8e2b618dd9e8" + integrity sha512-Xnz9zZvvQzUw+9DrOn0MomR7BxFCkA2pcfXBQuHC28ndJpSbjLs7knzYb05kw5SyCjSsEWombkZMgGcJSk8JVg== + dependencies: + "@opentelemetry/core" "2.8.0" + +"@opentelemetry/resources@2.8.0", "@opentelemetry/resources@^2.7.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.8.0.tgz#9bcb658ab6254f33099f4a95544b40d6f53cc946" + integrity sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg== + dependencies: + "@opentelemetry/core" "2.8.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/sdk-logs@0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz#78886fe300b82802cee9963208b2af326b928af5" - integrity sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag== +"@opentelemetry/sdk-logs@0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz#002433237908d24fa9e7f17eb4963f25f03d655c" + integrity sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA== dependencies: - "@opentelemetry/api-logs" "0.218.0" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/resources" "2.7.1" + "@opentelemetry/api-logs" "0.219.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/sdk-metrics@2.7.1", "@opentelemetry/sdk-metrics@^2.7.0": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz#b713f69dd67933ecc9c61357f1d452cdc9f4e281" - integrity sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ== - dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/resources" "2.7.1" - -"@opentelemetry/sdk-node@^0.218.0": - version "0.218.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz#cf8bdc0c993387189c7c474612e0f801505feb97" - integrity sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg== - dependencies: - "@opentelemetry/api-logs" "0.218.0" - "@opentelemetry/configuration" "0.218.0" - "@opentelemetry/context-async-hooks" "2.7.1" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/exporter-logs-otlp-grpc" "0.218.0" - "@opentelemetry/exporter-logs-otlp-http" "0.218.0" - "@opentelemetry/exporter-logs-otlp-proto" "0.218.0" - "@opentelemetry/exporter-metrics-otlp-grpc" "0.218.0" - "@opentelemetry/exporter-metrics-otlp-http" "0.218.0" - "@opentelemetry/exporter-metrics-otlp-proto" "0.218.0" - "@opentelemetry/exporter-prometheus" "0.218.0" - "@opentelemetry/exporter-trace-otlp-grpc" "0.218.0" - "@opentelemetry/exporter-trace-otlp-http" "0.218.0" - "@opentelemetry/exporter-trace-otlp-proto" "0.218.0" - "@opentelemetry/exporter-zipkin" "2.7.1" - "@opentelemetry/instrumentation" "0.218.0" - "@opentelemetry/otlp-exporter-base" "0.218.0" - "@opentelemetry/propagator-b3" "2.7.1" - "@opentelemetry/propagator-jaeger" "2.7.1" - "@opentelemetry/resources" "2.7.1" - "@opentelemetry/sdk-logs" "0.218.0" - "@opentelemetry/sdk-metrics" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" - "@opentelemetry/sdk-trace-node" "2.7.1" +"@opentelemetry/sdk-metrics@2.8.0", "@opentelemetry/sdk-metrics@^2.7.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz#c3f908d9e7e02fb855673fcfd4b298ce279e61f1" + integrity sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg== + dependencies: + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" + +"@opentelemetry/sdk-node@^0.219.0": + version "0.219.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-node/-/sdk-node-0.219.0.tgz#a269bfbe122249bcc2027b67cfdcca4bded254b5" + integrity sha512-NWLpWLEb8gV3+JBHYoIrktbM385wyHpRJoh3J/4Q52d4PR+AlPMNGJT3DzBUrDSUEVbKAXoHR+EDAPxtiNcj8g== + dependencies: + "@opentelemetry/api-logs" "0.219.0" + "@opentelemetry/configuration" "0.219.0" + "@opentelemetry/context-async-hooks" "2.8.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/exporter-logs-otlp-grpc" "0.219.0" + "@opentelemetry/exporter-logs-otlp-http" "0.219.0" + "@opentelemetry/exporter-logs-otlp-proto" "0.219.0" + "@opentelemetry/exporter-metrics-otlp-grpc" "0.219.0" + "@opentelemetry/exporter-metrics-otlp-http" "0.219.0" + "@opentelemetry/exporter-metrics-otlp-proto" "0.219.0" + "@opentelemetry/exporter-prometheus" "0.219.0" + "@opentelemetry/exporter-trace-otlp-grpc" "0.219.0" + "@opentelemetry/exporter-trace-otlp-http" "0.219.0" + "@opentelemetry/exporter-trace-otlp-proto" "0.219.0" + "@opentelemetry/exporter-zipkin" "2.8.0" + "@opentelemetry/instrumentation" "0.219.0" + "@opentelemetry/otlp-exporter-base" "0.219.0" + "@opentelemetry/otlp-grpc-exporter-base" "0.219.0" + "@opentelemetry/propagator-b3" "2.8.0" + "@opentelemetry/propagator-jaeger" "2.8.0" + "@opentelemetry/resources" "2.8.0" + "@opentelemetry/sdk-logs" "0.219.0" + "@opentelemetry/sdk-metrics" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" + "@opentelemetry/sdk-trace-node" "2.8.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/sdk-trace-base@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz#9160c3af9ef2219c26563abd136e22fb7d19b34f" - integrity sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw== +"@opentelemetry/sdk-trace-base@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz#ec9c1d69e2e6fba256c9df0c8e8d67d42386d52b" + integrity sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ== dependencies: - "@opentelemetry/core" "2.7.1" - "@opentelemetry/resources" "2.7.1" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/resources" "2.8.0" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/sdk-trace-node@2.7.1": - version "2.7.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz#54dedb8e77fa51a6d02fc2192097739266c82168" - integrity sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg== +"@opentelemetry/sdk-trace-node@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.8.0.tgz#cbf68f817a15bf4ca5d9ff9fb60ef3a73f61337d" + integrity sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg== dependencies: - "@opentelemetry/context-async-hooks" "2.7.1" - "@opentelemetry/core" "2.7.1" - "@opentelemetry/sdk-trace-base" "2.7.1" + "@opentelemetry/context-async-hooks" "2.8.0" + "@opentelemetry/core" "2.8.0" + "@opentelemetry/sdk-trace-base" "2.8.0" "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.40.0": version "1.40.0" From 51c4bcb1cfd98df46e6b8cd83e9c6af1e8b13785 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:28:20 +0000 Subject: [PATCH 10/17] Bump hono from 4.12.18 to 4.12.26 Bumps [hono](https://github.com/honojs/hono) from 4.12.18 to 4.12.26. - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.12.18...v4.12.26) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.26 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- yarn.lock | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d61f6..430b6bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@levalicious/server-memory", - "version": "0.0.25", + "version": "0.0.26", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.29.0", @@ -4602,9 +4602,9 @@ } }, "node_modules/hono": { - "version": "4.12.18", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", - "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/yarn.lock b/yarn.lock index f957f93..8e6ea22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2448,9 +2448,9 @@ hasown@^2.0.2: function-bind "^1.1.2" hono@^4.11.4: - version "4.12.18" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.18.tgz#f6d301938868c3a8bdb639495f4e326a19181505" - integrity sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ== + version "4.12.26" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.26.tgz#450edfd64aad96cccc36829d63ec1430272e3ef8" + integrity sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw== html-escaper@^2.0.0: version "2.0.2" From 31fed00cc930909eb6d1eec867a2a19b35338207 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 04:11:17 +0000 Subject: [PATCH 11/17] Bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/automerge.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/sync-version.yml | 2 +- .github/workflows/typescript.yml | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 19f8781..dcb2ad4 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -12,7 +12,7 @@ jobs: contents: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Configure Git run: | git config user.name 'github-actions[bot]' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1759a9b..ab51fcc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,7 +28,7 @@ jobs: build-mode: none steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: diff --git a/.github/workflows/sync-version.yml b/.github/workflows/sync-version.yml index b06e498..77eefb6 100644 --- a/.github/workflows/sync-version.yml +++ b/.github/workflows/sync-version.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index e4332e9..5d41616 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -14,7 +14,7 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: 22 @@ -35,7 +35,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: 22 @@ -63,7 +63,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: 22 @@ -83,7 +83,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: node-version: 22 From f5ecdd377c801c2d9e40a9868609c64ec1c399ee Mon Sep 17 00:00:00 2001 From: Levalicious <24482560+Levalicious@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:29:46 -0400 Subject: [PATCH 12/17] memfile v3: pure-C graph store, ops in C, transparent auto-migration Replace the v1/v2 storage with a from-scratch pure-C graph store. The old biscuit-derived free-list could double-free into a 1-node cycle and hang; v3 eliminates that by redesign (Cartesian-tree allocator, sized free, continuous coalescing) rather than guarding against it. native/ memoryfile v3 mmap arena + Cartesian-tree free allocator (zero live metadata); refuses non-v3 files stringtable interned, refcounted strings graph entities/relations/observations, adjacency, persistent name index, BFS neighbors/find_path (best-effort + byte budget), POSIX-ERE search, MC pagerank + MERW psi, random walk, validate graphbind N-API binding (the C store IS the DB) entity.h/schema.h biscuit-style versioned records (X-macro lens chain) server.ts is now a thin MCP shell over the C ops (pagination, sort/format, NL-guard, walker-visit recording, tracing); src/store.ts wraps the binding. Search is case-sensitive POSIX ERE (dialect surfaced in the no-match warning). Migration (src/migrate.ts): logical-rebuild old v1/v2 -> v3 preserving mtime/obsMtime/visits/psi/totals. Auto-migrates in place on open (ensureV3): flock-serialized across detect+migrate so concurrent startups can't race or empty-clobber; keeps a .premigrate backup. Validated on a copy of the live 67k-entity / 146k-relation KB (0 mismatches); proven safe under 4 concurrent startups. Live files are only read, never the originals. binding.gyp: graphstore target, no -lnode (fixes the Node 24 exit segfault), -fno-math-errno (drops libm). Tests: the existing jest suite passes UNMODIFIED; 168/168 (adds migrate.test). native/Makefile builds the C fuzz/unit harnesses under ASan/UBSan with a test-only double-free detector (MEMFILE_DOUBLE_FREE_CHECK) that is absent from the shipped binary; clean across ~900k fuzz ops. Remove dead JS ranking (pagerank.ts, merw.ts) -- now computed in C. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfzTKfjTRRNQ828crWoJf1 --- binding.gyp | 13 +- native/Makefile | 38 ++ native/bench/.gitignore | 6 + native/bench/Makefile | 22 + native/bench/bench.c | 151 +++++ native/bench/mf_cart.c | 138 +++++ native/bench/mf_cart.h | 27 + native/bench/mf_radix.c | 113 ++++ native/bench/mf_radix.h | 29 + native/bench/substrate.c | 89 +++ native/bench/substrate.h | 54 ++ native/bench/test_binding.cjs | 48 ++ native/bench/test_cart.c | 66 +++ native/bench/test_coalesce.c | 56 ++ native/entity.h | 103 ++++ native/fc_harness.c | 34 ++ native/fc_index.c | 49 ++ native/fc_proofs.c | 45 ++ native/fc_stubs.h | 16 + native/fc_tree.c | 44 ++ native/graph.c | 999 ++++++++++++++++++++++++++++++++++ native/graph.h | 137 +++++ native/graphbind.c | 321 +++++++++++ native/memoryfile.c | 380 ++++++------- native/memoryfile.h | 62 +-- native/schema.h | 27 + native/stringtable.c | 222 ++++++++ native/stringtable.h | 43 ++ native/test_doublefree.c | 20 + native/test_entity.c | 33 ++ native/test_graph.c | 246 +++++++++ native/test_memfile.c | 71 +++ native/test_stringtable.c | 92 ++++ server.ts | 853 +++++++---------------------- src/memoryfile.ts | 12 +- src/merw.ts | 178 ------ src/migrate.ts | 280 ++++++++++ src/pagerank.ts | 89 --- src/store.ts | 184 +++++++ tests/migrate.test.ts | 94 ++++ 40 files changed, 4300 insertions(+), 1184 deletions(-) create mode 100644 native/Makefile create mode 100644 native/bench/.gitignore create mode 100644 native/bench/Makefile create mode 100644 native/bench/bench.c create mode 100644 native/bench/mf_cart.c create mode 100644 native/bench/mf_cart.h create mode 100644 native/bench/mf_radix.c create mode 100644 native/bench/mf_radix.h create mode 100644 native/bench/substrate.c create mode 100644 native/bench/substrate.h create mode 100644 native/bench/test_binding.cjs create mode 100644 native/bench/test_cart.c create mode 100644 native/bench/test_coalesce.c create mode 100644 native/entity.h create mode 100644 native/fc_harness.c create mode 100644 native/fc_index.c create mode 100644 native/fc_proofs.c create mode 100644 native/fc_stubs.h create mode 100644 native/fc_tree.c create mode 100644 native/graph.c create mode 100644 native/graph.h create mode 100644 native/graphbind.c create mode 100644 native/schema.h create mode 100644 native/stringtable.c create mode 100644 native/stringtable.h create mode 100644 native/test_doublefree.c create mode 100644 native/test_entity.c create mode 100644 native/test_graph.c create mode 100644 native/test_memfile.c create mode 100644 native/test_stringtable.c delete mode 100644 src/merw.ts create mode 100644 src/migrate.ts delete mode 100644 src/pagerank.ts create mode 100644 src/store.ts create mode 100644 tests/migrate.test.ts diff --git a/binding.gyp b/binding.gyp index 72eca2a..876c448 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,22 +1,25 @@ { "targets": [ { - "target_name": "memoryfile", + "target_name": "graphstore", "sources": [ "native/memoryfile.c", - "native/binding.c" + "native/stringtable.c", + "native/graph.c", + "native/graphbind.c" ], "include_dirs": [ "native" ], - "cflags": ["-std=c11", "-Wall", "-Wextra", "-O2"], + "cflags": ["-std=c11", "-Wall", "-Wextra", "-O2", "-fno-strict-aliasing", "-fno-math-errno"], "conditions": [ ["OS=='linux'", { - "defines": ["_GNU_SOURCE"] + "defines": ["_GNU_SOURCE"], + "libraries!": ["-lnode"] }], ["OS=='mac'", { "xcode_settings": { - "OTHER_CFLAGS": ["-std=c11", "-Wall", "-Wextra", "-O2"] + "OTHER_CFLAGS": ["-std=c11", "-Wall", "-Wextra", "-O2", "-fno-strict-aliasing", "-fno-math-errno"] } }] ] diff --git a/native/Makefile b/native/Makefile new file mode 100644 index 0000000..85c870d --- /dev/null +++ b/native/Makefile @@ -0,0 +1,38 @@ +# Dev-time test build for the v3 C store. +# +# Compiles the fuzz/unit harnesses with AddressSanitizer + UBSan AND the +# test-only double-free detector (-DMEMFILE_DOUBLE_FREE_CHECK). The PUBLISHED +# addon is built by binding.gyp / node-gyp, which NEVER defines that macro, so +# the detector is physically absent from the shipped binary (the production +# allocator stays clean — Principle_FixCauseNotChecks). +CC ?= cc +CFLAGS = -std=c11 -g -O1 -Wall -Wextra -fsanitize=address,undefined \ + -D_GNU_SOURCE -DMEMFILE_DOUBLE_FREE_CHECK -I. +LIBS = -lm +OUT = /tmp/mf_test + +.PHONY: test verify-detector test_memfile test_stringtable test_graph test_entity clean + +# `make test` = prove the detector fires, then run every harness with it active. +test: verify-detector test_memfile test_stringtable test_graph test_entity + +verify-detector: test_doublefree.c memoryfile.c + @$(CC) $(CFLAGS) test_doublefree.c memoryfile.c $(LIBS) -o $(OUT)_df + @if $(OUT)_df 2>&1 | grep -q "DOUBLE-FREE detected"; then \ + echo "ok: double-free detector fires (abort) on a deliberate double-free"; \ + else echo "FAIL: double-free detector did NOT fire"; exit 1; fi + +test_memfile: test_memfile.c memoryfile.c + $(CC) $(CFLAGS) $^ $(LIBS) -o $(OUT)_memfile && $(OUT)_memfile + +test_stringtable: test_stringtable.c stringtable.c memoryfile.c + $(CC) $(CFLAGS) $^ $(LIBS) -o $(OUT)_st && $(OUT)_st + +test_graph: test_graph.c graph.c stringtable.c memoryfile.c + $(CC) $(CFLAGS) $^ $(LIBS) -o $(OUT)_graph && $(OUT)_graph + +test_entity: test_entity.c + $(CC) $(CFLAGS) $^ $(LIBS) -o $(OUT)_entity && $(OUT)_entity + +clean: + rm -f $(OUT)_* diff --git a/native/bench/.gitignore b/native/bench/.gitignore new file mode 100644 index 0000000..dc038c7 --- /dev/null +++ b/native/bench/.gitignore @@ -0,0 +1,6 @@ +# Compiled allocator bench/test binaries (build via this dir's Makefile). +bench_cart +bench_radix +bench_radix_asan +test_cart +test_coalesce diff --git a/native/bench/Makefile b/native/bench/Makefile new file mode 100644 index 0000000..b15ad27 --- /dev/null +++ b/native/bench/Makefile @@ -0,0 +1,22 @@ +CC ?= gcc +CFLAGS ?= -std=c11 -O2 -Wall -Wextra -D_GNU_SOURCE -march=native +ASAN = -std=c11 -O1 -g -fsanitize=address,undefined -D_GNU_SOURCE +SUB = substrate.c + +.PHONY: all clean test +all: bench_radix bench_cart + +bench_radix: $(SUB) mf_radix.c bench.c substrate.h mf_radix.h + $(CC) $(CFLAGS) -DALLOC_RADIX -o $@ $(SUB) mf_radix.c bench.c + +bench_cart: $(SUB) mf_cart.c bench.c substrate.h mf_cart.h + $(CC) $(CFLAGS) -DALLOC_CART -o $@ $(SUB) mf_cart.c bench.c + +test: + $(CC) $(ASAN) -o test_coalesce $(SUB) mf_radix.c test_coalesce.c + $(CC) $(ASAN) -o test_cart $(SUB) mf_cart.c test_cart.c + ./test_coalesce + ./test_cart + +clean: + rm -f bench_radix bench_cart test_coalesce test_cart diff --git a/native/bench/bench.c b/native/bench/bench.c new file mode 100644 index 0000000..324b4c0 --- /dev/null +++ b/native/bench/bench.c @@ -0,0 +1,151 @@ +/* + * Allocator microbench. rdtsc per-op, reports cycle distribution + fragmentation. + * Build per-variant (inline, no vtable): -DALLOC_RADIX or -DALLOC_CART. + * + * ./bench_X [ops] [seed] [extern_coalesce_interval] [radix_threshold] + * extern_coalesce_interval : call coalesce every N ops at harness level (0 = off; default 65536) + * radix_threshold : radix self-coalesces when free_count exceeds this (0 = off; radix only) + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include + +#include "substrate.h" + +#if defined(ALLOC_RADIX) +# include "mf_radix.h" +# define A_ALLOC mfr_alloc +# define A_FREE mfr_free +# define A_COALESCE mfr_coalesce +# define A_FREELEN mfr_freelist_len +# define A_FREEBYTES mfr_free_bytes +# define A_NAME "radix" +# define A_SET_THRESHOLD(t) (mfr_coalesce_threshold = (u64)(t)) +# define A_COALESCE_CALLS (mfr_coalesce_calls) +#elif defined(ALLOC_CART) +# include "mf_cart.h" +# define A_ALLOC mfc_alloc +# define A_FREE mfc_free +# define A_COALESCE mfc_coalesce +# define A_FREELEN mfc_freelist_len +# define A_FREEBYTES mfc_free_bytes +# define A_NAME "cart" +# define A_SET_THRESHOLD(t) ((void)(t)) +# define A_COALESCE_CALLS ((u64)0) +#else +# error "define an allocator variant (-DALLOC_RADIX or -DALLOC_CART)" +#endif + +static inline u64 tsc_begin(void) { + unsigned a, d; + __asm__ __volatile__("lfence\n\trdtsc" : "=a"(a), "=d"(d)); + return ((u64)d << 32) | a; +} +static inline u64 tsc_end(void) { + unsigned a, d; + __asm__ __volatile__("rdtscp" : "=a"(a), "=d"(d) :: "ecx"); + __asm__ __volatile__("lfence" ::: "memory"); + return ((u64)d << 32) | a; +} + +static u64 g_rng; +static inline u64 xs(void) { u64 x = g_rng; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return g_rng = x; } + +/* record-size mix: ~entity (56B), ~edge cell (24B), strings 16..512B */ +static u64 pick_size(void) { + u64 r = xs() % 100; + if (r < 50) return 56; + if (r < 80) return 24; + return 16 + 16 * (xs() % 32); +} + +static int cmp_u64(const void *a, const void *b) { + u64 x = *(const u64 *)a, y = *(const u64 *)b; + return (x > y) - (x < y); +} +static void report(const char *name, u64 *v, size_t n) { + if (!n) { printf(" %-9s (none)\n", name); return; } + qsort(v, n, sizeof(u64), cmp_u64); + size_t p99 = (size_t)(n * 0.99); + if (p99 >= n) p99 = n - 1; + double mean = 0; + for (size_t i = 0; i < n; i++) mean += (double)v[i]; + mean /= (double)n; + printf(" %-9s n=%-9zu min=%-5llu med=%-5llu mean=%-8.1f p99=%-7llu max=%llu\n", + name, n, (unsigned long long)v[0], (unsigned long long)v[n / 2], + mean, (unsigned long long)v[p99], (unsigned long long)v[n - 1]); +} + +int main(int argc, char **argv) { + cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set); + sched_setaffinity(0, sizeof(set), &set); + + size_t ops = (argc > 1) ? strtoul(argv[1], NULL, 10) : 2000000; + g_rng = (argc > 2) ? strtoull(argv[2], NULL, 10) : 0x9e3779b97f4a7c15ull; + size_t coal_iv = (argc > 3) ? strtoul(argv[3], NULL, 10) : 65536; + u64 threshold = (argc > 4) ? strtoull(argv[4], NULL, 10) : 0; + A_SET_THRESHOLD(threshold); + + const char *path = "/tmp/mfbench.dat"; + unlink(path); + mf_t *mf = mf_open(path, 1u << 20); + if (!mf) { perror("mf_open"); return 1; } + + typedef struct { u64 off, size; } live_t; + size_t cap = 1u << 20; + live_t *live = malloc(cap * sizeof(*live)); + size_t nlive = 0; + u64 *at = malloc(ops * sizeof(u64)); size_t na = 0; + u64 *ft = malloc(ops * sizeof(u64)); size_t nf = 0; + u64 coal_cycles = 0; size_t coal_n = 0; + u64 fail = 0; + + for (size_t i = 0; i < ops; i++) { + int do_alloc = (nlive == 0) || (nlive < cap && (xs() & 1)); + if (do_alloc) { + u64 sz = pick_size(); + u64 t0 = tsc_begin(); + u64 off = A_ALLOC(mf, sz); + u64 t1 = tsc_end(); + if (off) { at[na++] = t1 - t0; live[nlive].off = off; live[nlive].size = sz; nlive++; } + else fail++; + } else { + size_t k = xs() % nlive; + live_t L = live[k]; + live[k] = live[--nlive]; + u64 t0 = tsc_begin(); + A_FREE(mf, L.off, L.size); + u64 t1 = tsc_end(); + ft[nf++] = t1 - t0; + } + if (coal_iv && i && (i % coal_iv) == 0) { + u64 c0 = tsc_begin(); + A_COALESCE(mf); + u64 c1 = tsc_end(); + coal_cycles += c1 - c0; coal_n++; + } + } + + u64 used = mf->hdr->allocated, freeb = A_FREEBYTES(mf), filesz = mf->hdr->file_size; + printf("alloc=%s ops=%zu seed=0x%llx fail=%llu coal_iv=%zu threshold=%llu internal_coal=%llu\n", + A_NAME, ops, (unsigned long long)g_rng, (unsigned long long)fail, + coal_iv, (unsigned long long)threshold, (unsigned long long)A_COALESCE_CALLS); + printf("state: live=%zu freelist=%llu free_bytes=%llu allocated=%llu file=%llu frag=%.3f\n", + nlive, (unsigned long long)A_FREELEN(mf), (unsigned long long)freeb, + (unsigned long long)used, (unsigned long long)filesz, + used ? (double)freeb / (double)used : 0.0); + printf("cycles/op:\n"); + report("alloc", at, na); + report("free", ft, nf); + if (coal_n) printf(" coalesce n=%zu mean=%.1f\n", coal_n, (double)coal_cycles / (double)coal_n); + + mf_close(mf); + free(live); free(at); free(ft); + return 0; +} diff --git a/native/bench/mf_cart.c b/native/bench/mf_cart.c new file mode 100644 index 0000000..69dc242 --- /dev/null +++ b/native/bench/mf_cart.c @@ -0,0 +1,138 @@ +#include "mf_cart.h" + +typedef struct __attribute__((packed)) { u64 size, left, right, parent; } mfc_node_t; + +static inline mfc_node_t *N(mf_t *mf, u64 o) { return (mfc_node_t *)mf_ptr(mf, o); } + +static inline u64 round32(u64 s) { + s = (s + (MFC_QUANTUM - 1)) & ~(u64)(MFC_QUANTUM - 1); + return s < MFC_MIN_BLOCK ? MFC_MIN_BLOCK : s; +} + +/* Rotate child x above its parent p, preserving BST order. Updates parent links + root. */ +static void rot(mf_t *mf, u64 x) { + u64 p = N(mf, x)->parent; + u64 g = N(mf, p)->parent; + if (N(mf, p)->left == x) { /* right rotation */ + u64 b = N(mf, x)->right; + N(mf, x)->right = p; N(mf, p)->parent = x; + N(mf, p)->left = b; if (b) N(mf, b)->parent = p; + } else { /* left rotation */ + u64 b = N(mf, x)->left; + N(mf, x)->left = p; N(mf, p)->parent = x; + N(mf, p)->right = b; if (b) N(mf, b)->parent = p; + } + N(mf, x)->parent = g; + if (!g) mf->hdr->free_root = x; + else if (N(mf, g)->left == p) N(mf, g)->left = x; else N(mf, g)->right = x; +} + +/* Insert a free block (addr = off, BST key) maintaining the max-heap on size. */ +static void cart_insert(mf_t *mf, u64 off, u64 size) { + N(mf, off)->size = size; N(mf, off)->left = 0; N(mf, off)->right = 0; N(mf, off)->parent = 0; + mf->hdr->free_bytes += size; mf->hdr->free_count += 1; + + u64 root = mf->hdr->free_root; + if (!root) { mf->hdr->free_root = off; return; } + + u64 cur = root, par = 0; + while (cur) { par = cur; cur = (off < cur) ? N(mf, cur)->left : N(mf, cur)->right; } + N(mf, off)->parent = par; + if (off < par) N(mf, par)->left = off; else N(mf, par)->right = off; + + while (N(mf, off)->parent && N(mf, N(mf, off)->parent)->size < N(mf, off)->size) + rot(mf, off); +} + +/* Remove a node: rotate it down (toward the larger child) to a leaf, then detach. */ +static void cart_remove(mf_t *mf, u64 off) { + for (;;) { + u64 l = N(mf, off)->left, r = N(mf, off)->right; + if (!l && !r) break; + u64 bigger = !l ? r : !r ? l : (N(mf, l)->size >= N(mf, r)->size ? l : r); + rot(mf, bigger); + } + u64 p = N(mf, off)->parent; + if (!p) mf->hdr->free_root = 0; + else if (N(mf, p)->left == off) N(mf, p)->left = 0; else N(mf, p)->right = 0; + mf->hdr->free_bytes -= N(mf, off)->size; mf->hdr->free_count -= 1; +} + +/* Lowest-address free block with size >= n, in O(h). Uses heap: left subtree max = left->size. */ +static u64 cart_fit(mf_t *mf, u64 n) { + u64 cur = mf->hdr->free_root; + while (cur) { + u64 l = N(mf, cur)->left; + if (l && N(mf, l)->size >= n) { cur = l; continue; } + if (N(mf, cur)->size >= n) return cur; + cur = N(mf, cur)->right; + } + return 0; +} + +static u64 cart_succ(mf_t *mf, u64 off) { /* smallest address > off */ + u64 cur = mf->hdr->free_root, s = 0; + while (cur) { if (cur > off) { s = cur; cur = N(mf, cur)->left; } else cur = N(mf, cur)->right; } + return s; +} +static u64 cart_pred(mf_t *mf, u64 off) { /* largest address < off */ + u64 cur = mf->hdr->free_root, p = 0; + while (cur) { if (cur < off) { p = cur; cur = N(mf, cur)->right; } else cur = N(mf, cur)->left; } + return p; +} + +u64 mfc_alloc(mf_t *mf, u64 size) { + u64 n = round32(size); + u64 b = cart_fit(mf, n); + if (b) { + u64 bsz = N(mf, b)->size; + cart_remove(mf, b); + u64 rem = bsz - n; /* 0 or >= 32 */ + if (rem >= MFC_MIN_BLOCK) cart_insert(mf, b + n, rem); + return b; + } + if (mf_ensure(mf, n) < 0) return 0; + u64 off = mf->hdr->allocated; + mf->hdr->allocated += n; + return off; +} + +void mfc_free(mf_t *mf, u64 off, u64 size) { + if (!off) return; + u64 n = round32(size); + + /* merge with the immediately-following free block, if physically adjacent */ + u64 s = cart_succ(mf, off); + if (s && off + n == s) { n += N(mf, s)->size; cart_remove(mf, s); } + + /* merge with the immediately-preceding free block, if physically adjacent */ + u64 p = cart_pred(mf, off); + if (p && p + N(mf, p)->size == off) { u64 psz = N(mf, p)->size; cart_remove(mf, p); off = p; n += psz; } + + cart_insert(mf, off, n); +} + +void mfc_coalesce(mf_t *mf) { (void)mf; } /* continuous */ + +u64 mfc_freelist_len(mf_t *mf) { return mf->hdr->free_count; } +u64 mfc_free_bytes(mf_t *mf) { return mf->hdr->free_bytes; } + +/* ---- test-only invariant checker ---- */ +static void v_rec(mf_t *mf, u64 node, u64 parent, u64 lo, u64 hi, + u64 *count, u64 *bytes, int *ok) { + if (!node) return; + if (N(mf, node)->parent != parent) *ok = 0; /* parent link */ + if (node <= lo || node >= hi) *ok = 0; /* BST order by address */ + if (parent && N(mf, parent)->size < N(mf, node)->size) *ok = 0; /* max-heap on size */ + (*count)++; (*bytes) += N(mf, node)->size; + v_rec(mf, N(mf, node)->left, node, lo, node, count, bytes, ok); + v_rec(mf, N(mf, node)->right, node, node, hi, count, bytes, ok); +} + +int mfc_validate(mf_t *mf) { + int ok = 1; u64 count = 0, bytes = 0; + v_rec(mf, mf->hdr->free_root, 0, 0, ~(u64)0, &count, &bytes, &ok); + if (count != mf->hdr->free_count) ok = 0; + if (bytes != mf->hdr->free_bytes) ok = 0; + return ok ? 0 : 1; +} diff --git a/native/bench/mf_cart.h b/native/bench/mf_cart.h new file mode 100644 index 0000000..0749c97 --- /dev/null +++ b/native/bench/mf_cart.h @@ -0,0 +1,27 @@ +/* + * mf_cart - ConstantSpaceAllocator, Cartesian-tree (Stephenson "Fast Fits") variant. + * + * - Zero live-block metadata. Sized free. 32B-quantized (node {size,left,right,parent}). + * - The free set is a Cartesian tree resident in the free blocks: + * BST keyed by ADDRESS (the node's own offset) -> address-neighbors for coalescing + * max-HEAP keyed by SIZE -> O(h) lowest-address fit + * - Coalescing is CONTINUOUS: free() merges with the address-adjacent free neighbors + * (the tree invariant guarantees no two free blocks are ever physically adjacent). + * - No balancing yet: literal Fast-Fits. Worst-case depth O(n) under size/address skew. + */ +#ifndef MF_CART_H +#define MF_CART_H + +#include "substrate.h" + +#define MFC_MIN_BLOCK 32u /* sizeof {u64 size, left, right, parent} */ +#define MFC_QUANTUM 32u + +u64 mfc_alloc(mf_t *mf, u64 size); /* returns offset, 0 = fail */ +void mfc_free(mf_t *mf, u64 off, u64 size); /* sized free; coalesces continuously */ +void mfc_coalesce(mf_t *mf); /* no-op: coalescing is continuous */ +u64 mfc_freelist_len(mf_t *mf); /* free_count (O(1)) */ +u64 mfc_free_bytes(mf_t *mf); /* free_bytes (O(1)) */ +int mfc_validate(mf_t *mf); /* test-only: 0 ok; checks BST+heap+parent+counters */ + +#endif /* MF_CART_H */ diff --git a/native/bench/mf_radix.c b/native/bench/mf_radix.c new file mode 100644 index 0000000..3df3729 --- /dev/null +++ b/native/bench/mf_radix.c @@ -0,0 +1,113 @@ +#include "mf_radix.h" + +typedef struct __attribute__((packed)) { u64 size; u64 next; } mfr_free_t; + +u64 mfr_coalesce_threshold = 0; +u64 mfr_coalesce_calls = 0; + +static inline u64 round16(u64 s) { + s = (s + (MFR_QUANTUM - 1)) & ~(u64)(MFR_QUANTUM - 1); + return s < MFR_MIN_BLOCK ? MFR_MIN_BLOCK : s; +} +static inline mfr_free_t *fb(mf_t *mf, u64 off) { return (mfr_free_t *)mf_ptr(mf, off); } + +u64 mfr_alloc(mf_t *mf, u64 size) { + u64 n = round16(size); + + u64 prev = 0, cur = mf->hdr->free_root; + while (cur) { + mfr_free_t *b = fb(mf, cur); + if (b->size >= n) { + u64 rem = b->size - n; /* 0 or >= 16 (all multiples of 16) */ + u64 next = b->next; + if (rem >= MFR_MIN_BLOCK) { + u64 lo = cur + n; + mfr_free_t *lb = fb(mf, lo); + lb->size = rem; + lb->next = next; + if (prev) fb(mf, prev)->next = lo; else mf->hdr->free_root = lo; + mf->hdr->free_bytes -= n; /* remainder stays; count unchanged */ + } else { + if (prev) fb(mf, prev)->next = next; else mf->hdr->free_root = next; + mf->hdr->free_bytes -= n; /* exact fit (rem==0) */ + mf->hdr->free_count -= 1; + } + return cur; + } + prev = cur; + cur = b->next; + } + + if (mf_ensure(mf, n) < 0) return 0; + u64 off = mf->hdr->allocated; + mf->hdr->allocated += n; + return off; +} + +void mfr_free(mf_t *mf, u64 off, u64 size) { + if (!off) return; + u64 n = round16(size); + mfr_free_t *b = fb(mf, off); + b->size = n; + b->next = mf->hdr->free_root; + mf->hdr->free_root = off; + mf->hdr->free_bytes += n; + mf->hdr->free_count += 1; + if (mfr_coalesce_threshold && mf->hdr->free_count > mfr_coalesce_threshold) + mfr_coalesce(mf); +} + +u64 mfr_freelist_len(mf_t *mf) { return mf->hdr->free_count; } +u64 mfr_free_bytes(mf_t *mf) { return mf->hdr->free_bytes; } + +/* LSD radix sort of the free list by offset (8 bits/pass, bucket sublists), then linear merge. */ +void mfr_coalesce(mf_t *mf) { + u64 head = mf->hdr->free_root; + if (!head) return; + mfr_coalesce_calls++; + + enum { BITS = 8, RADIX = 256 }; + int passes = 0; + for (u64 m = mf->hdr->file_size; m; m >>= BITS) passes++; + if (!passes) passes = 1; + + for (int p = 0; p < passes; p++) { + u64 bhead[RADIX], btail[RADIX]; + for (int i = 0; i < RADIX; i++) { bhead[i] = 0; btail[i] = 0; } + + for (u64 cur = head; cur; ) { + mfr_free_t *b = fb(mf, cur); + u64 next = b->next; + int d = (int)((cur >> (p * BITS)) & (RADIX - 1)); + if (!bhead[d]) bhead[d] = cur; else fb(mf, btail[d])->next = cur; + btail[d] = cur; + cur = next; + } + + u64 nh = 0, nt = 0; + for (int i = 0; i < RADIX; i++) { + if (!bhead[i]) continue; + if (!nh) nh = bhead[i]; else fb(mf, nt)->next = bhead[i]; + nt = btail[i]; + } + if (nt) fb(mf, nt)->next = 0; + head = nh; + } + + u64 count = 0; + for (u64 cur = head; cur; ) { + mfr_free_t *b = fb(mf, cur); + u64 next = b->next; + while (next && cur + b->size == next) { + mfr_free_t *nb = fb(mf, next); + b->size += nb->size; + next = nb->next; + b->next = next; + } + count++; + cur = next; + } + + mf->hdr->free_root = head; + mf->hdr->free_count = count; /* free_bytes unchanged: merging conserves bytes */ +} diff --git a/native/bench/mf_radix.h b/native/bench/mf_radix.h new file mode 100644 index 0000000..fdc3a01 --- /dev/null +++ b/native/bench/mf_radix.h @@ -0,0 +1,29 @@ +/* + * mf_radix - ConstantSpaceAllocator, explicit-coalesce variant. + * + * - Zero live-block metadata. A live block is `round16(size)` bytes of pure payload. + * - Sized deallocation: caller passes size to free; we recompute the canonical block size. + * - Free blocks hold {size, next} (16B) in their own bytes; LIFO singly-linked free list. + * - Coalescing is EXPLICIT and batched: LSD radix sort of the free list by offset, + * then one linear merge pass. No auxiliary F-sized array. + * - Optional self-trigger: when free_count exceeds mfr_coalesce_threshold, free() coalesces. + */ +#ifndef MF_RADIX_H +#define MF_RADIX_H + +#include "substrate.h" + +#define MFR_MIN_BLOCK 16u /* sizeof {u64 size, u64 next} */ +#define MFR_QUANTUM 16u /* == min block, so split leftovers are always valid free blocks */ + +u64 mfr_alloc(mf_t *mf, u64 size); /* returns offset, 0 = fail */ +void mfr_free(mf_t *mf, u64 off, u64 size); /* sized free */ +void mfr_coalesce(mf_t *mf); /* explicit batch radix coalesce */ +u64 mfr_freelist_len(mf_t *mf); /* free_count (O(1)) */ +u64 mfr_free_bytes(mf_t *mf); /* free_bytes (O(1)) */ + +/* If >0, mfr_free() coalesces whenever free_count exceeds this (0 = off). */ +extern u64 mfr_coalesce_threshold; +extern u64 mfr_coalesce_calls; /* diagnostic: coalesce passes performed */ + +#endif /* MF_RADIX_H */ diff --git a/native/bench/substrate.c b/native/bench/substrate.c new file mode 100644 index 0000000..9e4747c --- /dev/null +++ b/native/bench/substrate.c @@ -0,0 +1,89 @@ +#include "substrate.h" + +#include +#include +#include +#include +#include +#include + +static int mf_remap(mf_t *mf, size_t new_size) { + if (ftruncate(mf->fd, new_size) < 0) return -1; +#ifdef __linux__ + void *nb = mremap(mf->base, mf->size, new_size, MREMAP_MAYMOVE); + if (nb == MAP_FAILED) return -1; +#else + munmap(mf->base, mf->size); + void *nb = mmap(NULL, new_size, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); + if (nb == MAP_FAILED) return -1; +#endif + mf->base = nb; + mf->size = new_size; + mf->hdr = (mf_header_t *)nb; + mf->hdr->file_size = new_size; + return 0; +} + +int mf_ensure(mf_t *mf, u64 need) { + if (mf->hdr->allocated + need <= mf->hdr->file_size) return 0; + size_t ns = mf->size * 2; + if (ns < mf->hdr->allocated + need) ns = mf->hdr->allocated + need + 4096; + return mf_remap(mf, ns); +} + +mf_t *mf_open(const char *path, size_t initial_size) { + mf_t *mf = calloc(1, sizeof(mf_t)); + if (!mf) return NULL; + mf->fd = -1; + mf->path = strdup(path); + + struct stat st; + int exists = (stat(path, &st) == 0 && st.st_size > 0); + + if (exists) { + mf->fd = open(path, O_RDWR); + if (mf->fd < 0) goto fail; + mf->size = (size_t)st.st_size; + mf->base = mmap(NULL, mf->size, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); + if (mf->base == MAP_FAILED) goto fail_fd; + mf->hdr = (mf_header_t *)mf->base; + if (mf->hdr->magic != MF_MAGIC) { munmap(mf->base, mf->size); goto fail_fd; } + } else { + mf->fd = open(path, O_RDWR | O_CREAT, 0644); + if (mf->fd < 0) goto fail; + if (initial_size < sizeof(mf_header_t) + 64) initial_size = 4096; + if (ftruncate(mf->fd, initial_size) < 0) goto fail_fd; + mf->size = initial_size; + mf->base = mmap(NULL, mf->size, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); + if (mf->base == MAP_FAILED) { unlink(path); goto fail_fd; } + mf->hdr = (mf_header_t *)mf->base; + mf->hdr->magic = MF_MAGIC; + mf->hdr->version = MF_VERSION; + mf->hdr->file_size = initial_size; + mf->hdr->allocated = sizeof(mf_header_t); + mf->hdr->free_root = 0; + mf->hdr->free_bytes = 0; + mf->hdr->free_count = 0; + } + return mf; + +fail_fd: + close(mf->fd); +fail: + free(mf->path); + free(mf); + return NULL; +} + +void mf_sync(mf_t *mf) { + if (mf && !mf->closed && mf->base) msync(mf->base, mf->size, MS_SYNC); +} + +void mf_close(mf_t *mf) { + if (!mf || mf->closed) return; + mf->closed = 1; + if (mf->base) munmap(mf->base, mf->size); + if (mf->fd >= 0) close(mf->fd); + free(mf->path); + free(mf); +} diff --git a/native/bench/substrate.h b/native/bench/substrate.h new file mode 100644 index 0000000..fecec60 --- /dev/null +++ b/native/bench/substrate.h @@ -0,0 +1,54 @@ +/* + * mf substrate - shared file/mmap/growth core for memfile v3 allocator benches. + * + * Owns: fd, mmap, mremap growth, the fixed header. Does NOT own the allocator: + * `free_root` is interpreted by the allocator variant (free-list head, or tree root). + * All allocations are OFFSETS from the file start. Pointers are invalid after growth. + */ +#ifndef MF_SUBSTRATE_H +#define MF_SUBSTRATE_H + +#include +#include + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +#define MF_MAGIC 0x4D454D46u /* "MEMF" */ +#define MF_VERSION 3 + +/* Fixed header at offset 0. Padded to 64B so `allocated` starts 32-aligned (tree variant). */ +typedef struct __attribute__((packed)) { + u32 magic; + u32 version; + u64 file_size; /* current file/mmap size */ + u64 allocated; /* bump cursor: next bump allocation starts here */ + u64 free_root; /* allocator-interpreted: free-list head OR tree root (0 = empty) */ + u64 free_bytes; /* total bytes currently free (sum of free blocks); O(1) maintained */ + u64 free_count; /* number of free blocks/nodes; O(1) maintained */ + u64 _pad[2]; /* pad to 64B */ +} mf_header_t; + +typedef struct { + int fd; + char *path; + void *base; /* mmap base */ + size_t size; /* mmap size */ + mf_header_t *hdr; /* == base */ + int closed; +} mf_t; + +mf_t *mf_open(const char *path, size_t initial_size); +void mf_close(mf_t *mf); +void mf_sync(mf_t *mf); + +/* Ensure `need` bytes are available above `allocated`; may remap (invalidates pointers). 0 ok, -1 fail. */ +int mf_ensure(mf_t *mf, u64 need); + +static inline void *mf_ptr(mf_t *mf, u64 off) { + return off ? (u8 *)mf->base + off : (void *)0; +} + +#endif /* MF_SUBSTRATE_H */ diff --git a/native/bench/test_binding.cjs b/native/bench/test_binding.cjs new file mode 100644 index 0000000..bb0bc94 --- /dev/null +++ b/native/bench/test_binding.cjs @@ -0,0 +1,48 @@ +// Smoke test for the v3 graphstore N-API addon (C<->JS marshaling). +const native = require('../../build/Release/graphstore.node'); +const fs = require('fs'); +const j = (v) => JSON.stringify(v, (k, x) => (typeof x === 'bigint' ? x.toString() : x)); + +for (const f of ['/tmp/gb_g.dat', '/tmp/gb_s.dat']) { try { fs.unlinkSync(f); } catch {} } +const h = native.open('/tmp/gb_g.dat', '/tmp/gb_s.dat', 65536); + +const alice = native.createEntity(h, 'Alice', 'Person', 100n); +const bob = native.createEntity(h, 'Bob', 'Person', 100n); +const acme = native.createEntity(h, 'Acme', 'Org', 100n); +native.createRelation(h, alice, bob, 'KNOWS', 100n); +native.createRelation(h, alice, acme, 'WORKS_AT', 100n); +native.addObservation(h, alice, 'likes tea', 100n); + +let ok = true; +const expect = (cond, msg) => { if (!cond) { ok = false; console.log('FAIL:', msg); } else console.log('ok: ', msg); }; + +expect(native.entityCount(h) === 3, 'entityCount == 3'); +expect(native.relationCount(h) === 2, 'relationCount == 2'); +expect(native.lookup(h, 'Alice') === alice, 'lookup Alice round-trips'); +expect(native.lookup(h, 'Nobody') === 0n, 'lookup miss -> 0n'); + +const arec = native.readEntity(h, alice); +expect(arec.name === 'Alice' && arec.type === 'Person', 'readEntity name/type'); +expect(arec.observations.length === 1 && arec.observations[0] === 'likes tea', 'readEntity observations'); + +const nbrs = native.neighbors(h, alice, 1, 0).map((o) => native.entityName(h, o)).sort(); +expect(j(nbrs) === j(['Acme', 'Bob']), 'neighbors(Alice, fwd, d1) = [Acme, Bob]'); + +const path = native.findPath(h, bob, acme, 4, 255).map((o) => native.entityName(h, o)); +expect(j(path) === j(['Bob', 'Alice', 'Acme']), 'findPath Bob->Acme (any) = Bob,Alice,Acme'); + +expect(native.search(h, 'Person').length === 2, 'search Person -> 2'); +expect(native.entityTypes(h).sort().join(',') === 'Org,Person', 'entityTypes'); +expect(native.relationTypes(h).sort().join(',') === 'KNOWS,WORKS_AT', 'relationTypes'); + +native.structuralSample(h, 1, 0.85); +native.computeMerwPsi(h, 0.85, 200, 1e-8); +expect(native.structuralTotal(h) > 0n, 'structural sampling recorded visits'); +expect(native.getPsi(h, alice) > 0, 'merw psi populated'); +native.incWalkerVisit(h, alice); +expect(native.walkerTotal(h) === 1n && native.walkerRank(h, alice) > 0, 'walker counting works'); + +native.sync(h); +native.close(h); +console.log(ok ? '\nALL PASS' : '\nFAILED'); +process.exit(ok ? 0 : 1); // exits clean: binding.gyp drops -lnode, so no 2nd libnode/libstdc++ static-dtor teardown diff --git a/native/bench/test_cart.c b/native/bench/test_cart.c new file mode 100644 index 0000000..afc131d --- /dev/null +++ b/native/bench/test_cart.c @@ -0,0 +1,66 @@ +/* + * Correctness + fuzz test for the Cartesian-tree allocator. + * T1: alloc N, free all (scrambled order) -> continuous coalescing collapses to ONE block. + * T2: randomized alloc/free fuzz; validate BST+heap+parent+counter invariants every step. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include "substrate.h" +#include "mf_cart.h" + +static int fails = 0; +#define CHECK(c, msg) do { if (!(c)) { printf(" FAIL: %s\n", msg); fails++; } \ + else printf(" ok: %s\n", msg); } while (0) + +static u64 g = 0x243f6a8885a308d3ull; +static u64 xs(void) { u64 x = g; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return g = x; } + +int main(void) { + const char *p = "/tmp/mf_cart_test.dat"; + unlink(p); + mf_t *mf = mf_open(p, 1u << 16); + if (!mf) { perror("mf_open"); return 2; } + + enum { N = 2000 }; + static u64 off[N], sz[N]; + + /* T1: alloc N, free all in scrambled order -> collapses to one block */ + for (int i = 0; i < N; i++) { sz[i] = 32 + 32 * (xs() % 8); off[i] = mfc_alloc(mf, sz[i]); } + CHECK(mfc_validate(mf) == 0, "T1 tree valid after N allocs"); + /* Fisher-Yates scramble of free order */ + for (int i = N - 1; i > 0; i--) { int j = (int)(xs() % (u64)(i + 1)); u64 t = off[i]; off[i] = off[j]; off[j] = t; u64 u = sz[i]; sz[i] = sz[j]; sz[j] = u; } + for (int i = 0; i < N; i++) mfc_free(mf, off[i], sz[i]); + CHECK(mfc_validate(mf) == 0, "T1 tree valid after freeing all"); + printf("T1 free-all-scrambled: free_count=%llu free_bytes=%llu\n", + (unsigned long long)mfc_freelist_len(mf), (unsigned long long)mfc_free_bytes(mf)); + CHECK(mfc_freelist_len(mf) == 1, "T1 collapses to a single free block"); + + /* T2: randomized fuzz with per-step invariant validation */ + typedef struct { u64 o, s; } live_t; + size_t cap = 4096; live_t *live = malloc(cap * sizeof(*live)); size_t nlive = 0; + size_t bad = 0; + for (size_t i = 0; i < 400000; i++) { + int do_alloc = (nlive == 0) || (nlive < cap && (xs() & 1)); + if (do_alloc) { + u64 s = 32 + 32 * (xs() % 16); + u64 o = mfc_alloc(mf, s); + if (o) { live[nlive].o = o; live[nlive].s = s; nlive++; } + } else { + size_t k = xs() % nlive; live_t L = live[k]; live[k] = live[--nlive]; + mfc_free(mf, L.o, L.s); + } + if ((i & 0x3FF) == 0 && mfc_validate(mf) != 0) bad++; + } + printf("T2 fuzz: ops=400000 final_live=%zu free_count=%llu invariant_failures=%zu\n", + nlive, (unsigned long long)mfc_freelist_len(mf), bad); + CHECK(bad == 0, "T2 invariants held across the fuzz run"); + + free(live); + mf_close(mf); + printf(fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", fails); + return fails ? 1 : 0; +} diff --git a/native/bench/test_coalesce.c b/native/bench/test_coalesce.c new file mode 100644 index 0000000..34c4476 --- /dev/null +++ b/native/bench/test_coalesce.c @@ -0,0 +1,56 @@ +/* + * Correctness test for the radix allocator's coalescing. + * Discriminates "real fragmentation" from "broken coalesce": + * T1: alloc N contiguous, free all, coalesce -> must collapse to ONE block. + * T2: free alternating -> no two adjacent -> stays ~N/2; free rest -> collapses to one. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include "substrate.h" +#include "mf_radix.h" + +static int fails = 0; +#define CHECK(c, msg) do { if (!(c)) { printf(" FAIL: %s\n", msg); fails++; } \ + else printf(" ok: %s\n", msg); } while (0) + +int main(void) { + const char *p = "/tmp/mf_coal_test.dat"; + unlink(p); + mf_t *mf = mf_open(p, 1u << 16); + if (!mf) { perror("mf_open"); return 2; } + + enum { N = 1000 }; + static u64 off[N]; + + /* T1: contiguous bump allocs, free all, coalesce -> 1 block */ + for (int i = 0; i < N; i++) off[i] = mfr_alloc(mf, 56); + for (int i = 0; i < N; i++) mfr_free(mf, off[i], 56); + u64 before = mfr_freelist_len(mf); + mfr_coalesce(mf); + u64 after = mfr_freelist_len(mf); + printf("T1 free-all: before=%llu after=%llu\n", + (unsigned long long)before, (unsigned long long)after); + CHECK(before == N, "T1 freelist length == N before coalesce"); + CHECK(after == 1, "T1 coalesces fully to a single block"); + + /* T2: alternating frees can't merge; then free the rest -> merges */ + for (int i = 0; i < N; i++) off[i] = mfr_alloc(mf, 56); + for (int i = 0; i < N; i += 2) mfr_free(mf, off[i], 56); + mfr_coalesce(mf); + u64 alt = mfr_freelist_len(mf); + printf("T2 alternating: after=%llu (expect ~%d)\n", (unsigned long long)alt, N / 2); + CHECK(alt >= (u64)(N / 2 - 1) && alt <= (u64)(N / 2 + 1), "T2 alternating frees do not merge"); + for (int i = 1; i < N; i += 2) mfr_free(mf, off[i], 56); + mfr_coalesce(mf); + u64 all = mfr_freelist_len(mf); + printf("T2 then free rest: after=%llu\n", (unsigned long long)all); + CHECK(all == 1, "T2 fully merges after all freed"); + + mf_close(mf); + printf(fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", fails); + return fails ? 1 : 0; +} diff --git a/native/entity.h b/native/entity.h new file mode 100644 index 0000000..dde0643 --- /dev/null +++ b/native/entity.h @@ -0,0 +1,103 @@ +// entity.h - TRUE single source of truth schema definition +#ifndef ENTITY_H +#define ENTITY_H + +#include "schema.h" + +//============================================================================= +// ENTITY_VERSIONS: The ONE AND ONLY definition of all versions and lenses +// +// BASE(version, struct_body) - first version, no lens +// V(version, prev_version, struct_body, lens_from_prev) - subsequent versions +//============================================================================= + +#define ENTITY_BASE \ + BASE(1, { u32 name_id; u32 type_id; u64 adj_offset; u64 mtime; u64 obs_mtime; \ + u8 obs_count; u8 _pad0[3]; u32 obs0_id; u32 obs1_id; u32 _pad1; \ + u64 structural_visits; u64 walker_visits; double psi; }) + +/* Future schema changes append one line each, e.g.: + * V(2, 1, { ; f32 score; }, + * { LENS_COPY(c->name_id, v->name_id); ...; c->score = 0; }) */ +#define ENTITY_UPGRADES + +#define ENTITY_CURRENT 1 + +//============================================================================= +// Expand: struct typedefs +//============================================================================= +#define BASE(ver, body) typedef struct body Entity_v##ver; +#define V(ver, prev, body, lens) typedef struct body Entity_v##ver; +ENTITY_BASE +ENTITY_UPGRADES +#undef BASE +#undef V + +typedef Entity_v1 Entity; +#define ENTITY_VERSION ENTITY_CURRENT + +//============================================================================= +// Expand: size table +//============================================================================= +#define BASE(ver, body) sizeof(Entity_v##ver), +#define V(ver, prev, body, lens) sizeof(Entity_v##ver), +static const size_t entity_sizes[] = { 0, ENTITY_BASE ENTITY_UPGRADES }; +#undef BASE +#undef V + +//============================================================================= +// Expand: lens functions - only from ENTITY_UPGRADES (base has no lens) +//============================================================================= +#define V(ver, prev, body, lens) \ + static inline void Entity_lens_##prev##_to_##ver(Entity_v##ver* c, const Entity_v##prev* v) lens +ENTITY_UPGRADES +#undef V + +//============================================================================= +// Expand: upgrade dispatch +//============================================================================= +static inline void entity_upgrade(int from_ver, void* out, const void* in) { + switch (from_ver) { +#define V(ver, prev, body, lens) \ + case prev: Entity_lens_##prev##_to_##ver((Entity_v##ver*)out, (const Entity_v##prev*)in); break; +ENTITY_UPGRADES +#undef V + } +} + +//============================================================================= +// Generic read: chains through lenses automatically +//============================================================================= +static inline int entity_read(const void* buf, Entity* out) { + u32 ver = *(const u32*)buf; + const void* data = (const char*)buf + sizeof(u32); + + if (ver == 0 || ver > ENTITY_VERSION) return -1; + + // Ping-pong buffers for chaining + u8 tmp[2][sizeof(Entity)]; + int cur = 0; + + memcpy(tmp[cur], data, entity_sizes[ver]); + + while (ver < ENTITY_VERSION) { + entity_upgrade(ver, tmp[1-cur], tmp[cur]); + cur = 1 - cur; + ver++; + } + + memcpy(out, tmp[cur], sizeof(Entity)); + return 0; +} + +static inline void entity_write(const Entity* c, void* buf) { + *(u32*)buf = ENTITY_VERSION; + memcpy((char*)buf + sizeof(u32), c, sizeof(Entity)); +} + +static inline size_t entity_bufsize(u32 version) { + if (version == 0 || version > ENTITY_VERSION) return 0; + return sizeof(u32) + entity_sizes[version]; +} + +#endif // ENTITY_H diff --git a/native/fc_harness.c b/native/fc_harness.c new file mode 100644 index 0000000..e32da34 --- /dev/null +++ b/native/fc_harness.c @@ -0,0 +1,34 @@ +/* + * Frama-C/EVA analysis harness for the Cartesian allocator. + * Backs the memfile with a STATIC buffer (no mmap/mremap) so abstract + * interpretation can model memory and check every deref/arith in alloc/free. + * Allocations stay within the buffer => no growth => memfile_remap never runs. + * + * frama-c -eva -main fc_main fc_harness.c memoryfile.c + */ +#include "memoryfile.h" + +#define MEMSZ (1u << 14) +static u8 G[MEMSZ]; + +int fc_main(void) { + memfile_t mf; + mf.fd = -1; mf.path = (char *)0; mf.mmap_base = G; mf.mmap_size = MEMSZ; + mf.header = (memfile_header_t *)G; mf.closed = 0; + mf.header->magic = MEMFILE_MAGIC; mf.header->version = MEMFILE_VERSION; + mf.header->file_size = MEMSZ; mf.header->allocated = sizeof(memfile_header_t); + mf.header->free_root = 0; mf.header->free_bytes = 0; mf.header->free_count = 0; + + /* a sequence touching split, take-whole, coalesce-forward/backward, tree rotations */ + u64 a = memfile_alloc(&mf, 40); + u64 b = memfile_alloc(&mf, 100); + u64 c = memfile_alloc(&mf, 32); + u64 d = memfile_alloc(&mf, 200); + memfile_free(&mf, b, 100); /* hole between a and c */ + memfile_free(&mf, c, 32); /* should coalesce with b's freed block */ + u64 e = memfile_alloc(&mf, 64);/* best-fit into a coalesced hole */ + memfile_free(&mf, a, 40); + memfile_free(&mf, d, 200); + memfile_free(&mf, e, 64); + return 0; +} diff --git a/native/fc_index.c b/native/fc_index.c new file mode 100644 index 0000000..1ddfa83 --- /dev/null +++ b/native/fc_index.c @@ -0,0 +1,49 @@ +/* + * ACSL/WP: the persistent name-index lookup, lifted onto a modeled memory region + * with the REAL on-disk offset arithmetic (16-byte buckets: {u32 name_id, u64 off} + * at idx + 8 + slot*16). Proves memory-safety (every bucket access in-bounds) and + * termination, for any bucket count, given the index block fits in memory. + * + * frama-c -wp -wp-rte -wp-prover alt-ergo,z3 fc_index.c + * + * Reads go through spec'd accessors (rd32/rd64) — the real code reads via memcpy + * for aliasing safety; here we model the read with an in-bounds contract so the + * proof is about the OFFSET ARITHMETIC, not libc memcpy's separation obligations. + * (bucket = hash32(name_id) % bc in real code; safety holds for any bucket < bc.) + */ +#include + +/*@ requires o + 8 <= msize; + @ requires \valid_read(mem + (0 .. msize - 1)); + @ assigns \nothing; */ +uint64_t rd64(unsigned char *mem, uint64_t msize, uint64_t o); + +/*@ requires o + 4 <= msize; + @ requires \valid_read(mem + (0 .. msize - 1)); + @ assigns \nothing; */ +uint32_t rd32(unsigned char *mem, uint64_t msize, uint64_t o); + +/*@ requires bc > 0 && bc <= 0x7fffffff; + @ requires \valid_read(mem + (0 .. msize - 1)); + @ requires idx + 8 + (uint64_t)bc * 16 <= msize; // index block fits in memory + @ assigns \nothing; + @*/ +uint64_t ni_lookup_model(unsigned char *mem, uint64_t msize, uint64_t idx, + uint32_t bc, uint32_t name_id) { + uint32_t bucket = name_id % bc; + /*@ loop invariant 0 <= i <= bc; + @ loop assigns i; + @ loop variant bc - i; + @*/ + for (uint32_t i = 0; i < bc; i++) { + uint32_t slot = (bucket + i) % bc; + //@ assert 0 <= slot < bc; + uint64_t base = idx + 8 + (uint64_t)slot * 16; + //@ assert base + 16 <= msize; + uint64_t off = rd64(mem, msize, base + 8); + uint32_t nid = rd32(mem, msize, base + 0); + if (off == 0) return 0; + if (nid == name_id) return off; + } + return 0; +} diff --git a/native/fc_proofs.c b/native/fc_proofs.c new file mode 100644 index 0000000..95c8119 --- /dev/null +++ b/native/fc_proofs.c @@ -0,0 +1,45 @@ +/* + * WP/ACSL proofs of v3 store algorithms (memory-model-clean abstractions of the + * real code, so WP can reason without the opaque mmap region). + * + * frama-c -wp -wp-rte -wp-prover alt-ergo,z3 fc_proofs.c + * + * 1) round_up32: the size-quantization guarantee the sized-free design rests on. + * 2) probe_find: the open-addressing linear probe used by the string table AND + * the persistent name index — proved memory-safe (no OOB bucket access) and + * terminating, for any bucket-count and key. + */ +#include + +/*@ requires size <= 0xffffffffffffffe0; // no wraparound on size + 31 + @ assigns \nothing; + @ ensures \result % 32 == 0; + @ ensures \result >= 32; + @ ensures \result >= size; + @*/ +uint64_t round_up32(uint64_t size) { + uint64_t q = (size + 31) / 32; + uint64_t s = q * 32; + return s < 32 ? 32 : s; +} + +/*@ requires bc > 0 && bc <= 0x7fffffff; + @ requires \valid_read(buckets + (0 .. bc - 1)); + @ assigns \nothing; + @ ensures \result == -1 || (0 <= \result < bc); + @ ensures \result >= 0 ==> buckets[\result] == key; + @*/ +int probe_find(uint64_t *buckets, unsigned bc, uint64_t key) { + unsigned bucket = (unsigned)(key % bc); + /*@ loop invariant 0 <= i <= bc; + @ loop assigns i; + @ loop variant bc - i; + @*/ + for (unsigned i = 0; i < bc; i++) { + unsigned slot = (bucket + i) % bc; + //@ assert 0 <= slot < bc; + if (buckets[slot] == 0) return -1; + if (buckets[slot] == key) return (int)slot; + } + return -1; +} diff --git a/native/fc_stubs.h b/native/fc_stubs.h new file mode 100644 index 0000000..29292b8 --- /dev/null +++ b/native/fc_stubs.h @@ -0,0 +1,16 @@ +/* + * Frama-C analysis-only stubs: declarations Frama-C's libc model lacks + * (GNU/Linux specifics). Force-included via -cpp-extra-args="-include fc_stubs.h". + * NEVER compiled into the real build. + */ +#ifndef FC_STUBS_H +#define FC_STUBS_H +#include + +#ifndef MREMAP_MAYMOVE +#define MREMAP_MAYMOVE 1 +#endif + +extern void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...); + +#endif /* FC_STUBS_H */ diff --git a/native/fc_tree.c b/native/fc_tree.c new file mode 100644 index 0000000..85715fe --- /dev/null +++ b/native/fc_tree.c @@ -0,0 +1,44 @@ +/* + * ACSL/WP: the Cartesian-tree fit descent (mf_fit), modeled over a node array. + * Proves MEMORY-SAFETY of the traversal: the cursor never leaves the node array, + * given the flat field-validity invariant (every left/right index is in range). + * + * frama-c -wp -wp-rte -wp-prover alt-ergo,z3 fc_tree.c + * + * NOTE ON TERMINATION: the descent terminates only if the tree is ACYCLIC. + * That acyclicity is exactly the structural invariant whose violation produced + * the original free-list HANG. WP cannot prove termination here without an + * acyclicity/height measure (an inductive reachability predicate + ghost state) — + * so this file proves safety; termination-under-acyclicity is the next frontier. + */ +#include + +#define NCAP 1048576u + +typedef struct { uint64_t size; uint32_t left, right, parent; } node; + +/*@ predicate fields_in_range(node *nodes, integer n) = + @ \forall integer k; 0 <= k < n ==> + @ 0 <= nodes[k].left < n && 0 <= nodes[k].right < n; + @*/ + +/*@ requires 0 < n <= NCAP; + @ requires 0 <= root < n; + @ requires \valid_read(nodes + (0 .. n - 1)); + @ requires fields_in_range(nodes, n); + @ assigns \nothing; + @ ensures 0 <= \result < n; + @*/ +uint32_t mf_fit_model(node *nodes, uint32_t n, uint32_t root, uint64_t need) { + uint32_t cur = root; + /*@ loop invariant 0 <= cur < n; + @ loop assigns cur; + @*/ + while (cur != 0) { + uint32_t l = nodes[cur].left; + if (l != 0 && nodes[l].size >= need) { cur = l; continue; } + if (nodes[cur].size >= need) return cur; + cur = nodes[cur].right; + } + return 0; +} diff --git a/native/graph.c b/native/graph.c new file mode 100644 index 0000000..c196c4e --- /dev/null +++ b/native/graph.c @@ -0,0 +1,999 @@ +#include "graph.h" + +#include +#include +#include +#include "entity.h" /* versioned record schema (single source of truth) */ + +#define GRAPH_HEADER_SIZE 40u /* node_log_off, structural_total, walker_total, name_index_off, schema_ver, pad */ + +/* graph header field offsets */ +#define GH_NODE_LOG_OFF 0 +#define GH_STRUCTURAL_TOTAL 8 +#define GH_WALKER_TOTAL 16 +#define GH_NAME_INDEX_OFF 24 +#define GH_SCHEMA_VERSION 32 + +/* entity record on-disk layout: [u32 version][Entity_v1 body]. Body fields are + * at sizeof(u32) + offsetof(Entity, field); the static_assert binds these to the + * entity.h single-source struct so they cannot silently drift. */ +#define E_VERSION 0 +#define E_NAME_ID 4 +#define E_TYPE_ID 8 +#define E_ADJ 12 +#define E_MTIME 20 +#define E_OBSM 28 +#define E_OBSCNT 36 +#define E_OBS0 40 +#define E_OBS1 44 +#define E_SVIS 52 +#define E_WVIS 60 +#define E_PSI 68 +_Static_assert(sizeof(u32) + sizeof(Entity) == ENTITY_RECORD_SIZE, "entity record size"); +_Static_assert(E_NAME_ID == sizeof(u32) + offsetof(Entity, name_id), "name_id offset"); +_Static_assert(E_ADJ == sizeof(u32) + offsetof(Entity, adj_offset), "adj offset"); +_Static_assert(E_PSI == sizeof(u32) + offsetof(Entity, psi), "psi offset"); + +/* adj entry field offsets */ +#define AE_TARGET_DIR 0 +#define AE_RELTYPE 8 +#define AE_MTIME 16 + +/* name-index block: [u32 bucket_count][u32 ni_count][bucket{u32 name_id,u32 pad,u64 offset}...] */ +#define NI_BUCKET_SIZE 16u + +/* ---- aliasing-safe field access ---- */ +static inline u8 rdu8 (memfile_t *mf, u64 o) { return *(const u8 *)memfile_ptr(mf, o); } +static inline u32 rdu32(memfile_t *mf, u64 o) { u32 v; memcpy(&v, memfile_ptr(mf, o), 4); return v; } +static inline u64 rdu64(memfile_t *mf, u64 o) { u64 v; memcpy(&v, memfile_ptr(mf, o), 8); return v; } +static inline double rdf64(memfile_t *mf, u64 o) { double v; memcpy(&v, memfile_ptr(mf, o), 8); return v; } +static inline void wru8 (memfile_t *mf, u64 o, u8 v) { *(u8 *)memfile_ptr(mf, o) = v; } +static inline void wru32(memfile_t *mf, u64 o, u32 v) { memcpy(memfile_ptr(mf, o), &v, 4); } +static inline void wru64(memfile_t *mf, u64 o, u64 v) { memcpy(memfile_ptr(mf, o), &v, 8); } +static inline void wrf64(memfile_t *mf, u64 o, double v) { memcpy(memfile_ptr(mf, o), &v, 8); } + +static inline u32 hash32(u32 x) { + x ^= x >> 16; x *= 0x7feb352du; x ^= x >> 15; x *= 0x846ca68bu; x ^= x >> 16; return x; +} + +/* ---- graph header accessors ---- */ +static inline u64 node_log_off(graph_t *g) { return rdu64(g->mf, g->header_offset + GH_NODE_LOG_OFF); } +static inline void set_node_log_off(graph_t *g, u64 v) { wru64(g->mf, g->header_offset + GH_NODE_LOG_OFF, v); } +static inline u64 name_index_off(graph_t *g) { return rdu64(g->mf, g->header_offset + GH_NAME_INDEX_OFF); } +static inline void set_name_index_off(graph_t *g, u64 v) { wru64(g->mf, g->header_offset + GH_NAME_INDEX_OFF, v); } + +/* ====================================================================== + * Persistent name index (name_id -> entity offset) + * ====================================================================== */ + +static inline u64 ni_bucket_pos(u64 idx, u32 slot) { return idx + 8 + (u64)slot * NI_BUCKET_SIZE; } + +static u64 ni_lookup(graph_t *g, u32 name_id) { + memfile_t *mf = g->mf; + u64 idx = name_index_off(g); + u32 bc = rdu32(mf, idx + 0); + u32 bucket = hash32(name_id) % bc; + for (u32 i = 0; i < bc; i++) { + u32 slot = (bucket + i) % bc; + u64 base = ni_bucket_pos(idx, slot); + u64 off = rdu64(mf, base + 8); + if (off == 0) return 0; + if (rdu32(mf, base + 0) == name_id) return off; + } + return 0; +} + +static void ni_rehash(graph_t *g, u32 new_bc); + +static void ni_insert(graph_t *g, u32 name_id, u64 offset) { + memfile_t *mf = g->mf; + u64 idx = name_index_off(g); + u32 bc = rdu32(mf, idx + 0); + u32 bucket = hash32(name_id) % bc; + for (u32 i = 0; i < bc; i++) { + u32 slot = (bucket + i) % bc; + u64 base = ni_bucket_pos(idx, slot); + if (rdu64(mf, base + 8) == 0) { + wru32(mf, base + 0, name_id); + wru64(mf, base + 8, offset); + u32 cnt = rdu32(mf, idx + 4) + 1; + wru32(mf, idx + 4, cnt); + if ((u64)cnt * 10 > (u64)bc * 7) ni_rehash(g, bc * 2); + return; + } + } +} + +static int ni_needs_reloc(u32 natural, u32 empty, u32 current) { + if (natural <= current) return natural <= empty && empty < current; + return natural <= empty || empty < current; +} +static void ni_fixup(graph_t *g, u32 removed, u32 bc) { + memfile_t *mf = g->mf; + u64 idx = name_index_off(g); + u32 slot = (removed + 1) % bc; + for (;;) { + u64 base = ni_bucket_pos(idx, slot); + if (rdu64(mf, base + 8) == 0) break; + u32 natural = hash32(rdu32(mf, base + 0)) % bc; + if (ni_needs_reloc(natural, removed, slot)) { + u64 rbase = ni_bucket_pos(idx, removed); + wru32(mf, rbase + 0, rdu32(mf, base + 0)); + wru64(mf, rbase + 8, rdu64(mf, base + 8)); + wru64(mf, base + 8, 0); + removed = slot; + } + slot = (slot + 1) % bc; + } +} +static void ni_remove(graph_t *g, u32 name_id) { + memfile_t *mf = g->mf; + u64 idx = name_index_off(g); + u32 bc = rdu32(mf, idx + 0); + u32 bucket = hash32(name_id) % bc; + for (u32 i = 0; i < bc; i++) { + u32 slot = (bucket + i) % bc; + u64 base = ni_bucket_pos(idx, slot); + if (rdu64(mf, base + 8) == 0) return; + if (rdu32(mf, base + 0) == name_id) { + wru64(mf, base + 8, 0); + wru32(mf, idx + 4, rdu32(mf, idx + 4) - 1); + ni_fixup(g, slot, bc); + return; + } + } +} +static void ni_rehash(graph_t *g, u32 new_bc) { + memfile_t *mf = g->mf; + u64 old_idx = name_index_off(g); + u32 old_bc = rdu32(mf, old_idx + 0); + u32 cnt = rdu32(mf, old_idx + 4); + u64 new_size = 8 + (u64)new_bc * NI_BUCKET_SIZE; + u64 new_idx = memfile_alloc(mf, new_size); + if (!new_idx) return; + memset(memfile_ptr(mf, new_idx), 0, new_size); + wru32(mf, new_idx + 0, new_bc); + wru32(mf, new_idx + 4, cnt); + for (u32 i = 0; i < old_bc; i++) { + u64 obase = ni_bucket_pos(old_idx, i); + u64 off = rdu64(mf, obase + 8); + if (off == 0) continue; + u32 nid = rdu32(mf, obase + 0); + u32 b = hash32(nid) % new_bc; + for (u32 j = 0; j < new_bc; j++) { + u32 s = (b + j) % new_bc; + u64 nbase = ni_bucket_pos(new_idx, s); + if (rdu64(mf, nbase + 8) == 0) { wru32(mf, nbase + 0, nid); wru64(mf, nbase + 8, off); break; } + } + } + set_name_index_off(g, new_idx); + memfile_free(mf, old_idx, 8 + (u64)old_bc * NI_BUCKET_SIZE); +} + +/* ====================================================================== + * Entity records + * ====================================================================== */ + +void graph_read_entity(graph_t *g, u64 off, entity_t *e) { + memfile_t *mf = g->mf; + e->offset = off; + e->name_id = rdu32(mf, off + E_NAME_ID); + e->type_id = rdu32(mf, off + E_TYPE_ID); + e->adj_offset = rdu64(mf, off + E_ADJ); + e->mtime = rdu64(mf, off + E_MTIME); + e->obs_mtime = rdu64(mf, off + E_OBSM); + e->obs_count = rdu8(mf, off + E_OBSCNT); + e->obs0_id = rdu32(mf, off + E_OBS0); + e->obs1_id = rdu32(mf, off + E_OBS1); + e->structural_visits = rdu64(mf, off + E_SVIS); + e->walker_visits = rdu64(mf, off + E_WVIS); + e->psi = rdf64(mf, off + E_PSI); +} + +/* ====================================================================== + * Node log + * ====================================================================== */ + +static void log_append(graph_t *g, u64 ent_off) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0); + u32 cap = rdu32(mf, log + 4); + if (count < cap) { + wru64(mf, log + NODE_LOG_HEADER_SIZE + (u64)count * 8, ent_off); + wru32(mf, log + 0, count + 1); + return; + } + u32 newcap = cap * 2; + u64 newlog = memfile_alloc(mf, NODE_LOG_HEADER_SIZE + (u64)newcap * 8); + if (!newlog) return; + wru32(mf, newlog + 0, count + 1); + wru32(mf, newlog + 4, newcap); + if (count) memcpy(memfile_ptr(mf, newlog + NODE_LOG_HEADER_SIZE), + memfile_ptr(mf, log + NODE_LOG_HEADER_SIZE), (u64)count * 8); + wru64(mf, newlog + NODE_LOG_HEADER_SIZE + (u64)count * 8, ent_off); + memfile_free(mf, log, NODE_LOG_HEADER_SIZE + (u64)cap * 8); + set_node_log_off(g, newlog); +} + +static void log_remove(graph_t *g, u64 ent_off) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0); + for (u32 i = 0; i < count; i++) { + if (rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8) == ent_off) { + u32 last = count - 1; + if (i < last) + wru64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8, + rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)last * 8)); + wru32(mf, log + 0, last); + return; + } + } +} + +/* ====================================================================== + * Adjacency + * ====================================================================== */ + +static void write_adj_entry(memfile_t *mf, u64 pos, const adj_entry_t *e) { + wru64(mf, pos + AE_TARGET_DIR, (e->target_offset << 2) | (e->direction & 3u)); + wru32(mf, pos + AE_RELTYPE, e->rel_type_id); + wru32(mf, pos + 12, 0); + wru64(mf, pos + AE_MTIME, e->mtime); +} + +void graph_add_edge(graph_t *g, u64 ent_off, const adj_entry_t *e) { + memfile_t *mf = g->mf; + u64 adj = rdu64(mf, ent_off + E_ADJ); + if (adj == 0) { + u64 sz = ADJ_HEADER_SIZE + (u64)INITIAL_ADJ_CAPACITY * ADJ_ENTRY_SIZE; + u64 noff = memfile_alloc(mf, sz); + if (!noff) return; + wru32(mf, noff + 0, 1); + wru32(mf, noff + 4, INITIAL_ADJ_CAPACITY); + write_adj_entry(mf, noff + ADJ_HEADER_SIZE, e); + wru64(mf, ent_off + E_ADJ, noff); + return; + } + u32 count = rdu32(mf, adj + 0); + u32 cap = rdu32(mf, adj + 4); + if (count < cap) { + write_adj_entry(mf, adj + ADJ_HEADER_SIZE + (u64)count * ADJ_ENTRY_SIZE, e); + wru32(mf, adj + 0, count + 1); + return; + } + u32 newcap = cap * 2; + u64 nadj = memfile_alloc(mf, ADJ_HEADER_SIZE + (u64)newcap * ADJ_ENTRY_SIZE); + if (!nadj) return; + wru32(mf, nadj + 0, count + 1); + wru32(mf, nadj + 4, newcap); + if (count) memcpy(memfile_ptr(mf, nadj + ADJ_HEADER_SIZE), + memfile_ptr(mf, adj + ADJ_HEADER_SIZE), (u64)count * ADJ_ENTRY_SIZE); + write_adj_entry(mf, nadj + ADJ_HEADER_SIZE + (u64)count * ADJ_ENTRY_SIZE, e); + memfile_free(mf, adj, ADJ_HEADER_SIZE + (u64)cap * ADJ_ENTRY_SIZE); + wru64(mf, ent_off + E_ADJ, nadj); +} + +int graph_remove_edge(graph_t *g, u64 ent_off, u64 target_off, u32 rel_type_id, u32 direction) { + memfile_t *mf = g->mf; + u64 adj = rdu64(mf, ent_off + E_ADJ); + if (adj == 0) return 0; + u32 count = rdu32(mf, adj + 0); + u64 packed = (target_off << 2) | (direction & 3u); + for (u32 i = 0; i < count; i++) { + u64 base = adj + ADJ_HEADER_SIZE + (u64)i * ADJ_ENTRY_SIZE; + if (rdu64(mf, base + AE_TARGET_DIR) == packed && rdu32(mf, base + AE_RELTYPE) == rel_type_id) { + u32 last = count - 1; + if (i < last) + memcpy(memfile_ptr(mf, base), + memfile_ptr(mf, adj + ADJ_HEADER_SIZE + (u64)last * ADJ_ENTRY_SIZE), ADJ_ENTRY_SIZE); + wru32(mf, adj + 0, last); + return 1; + } + } + return 0; +} + +u32 graph_edge_count(graph_t *g, u64 ent_off) { + u64 adj = rdu64(g->mf, ent_off + E_ADJ); + return adj ? rdu32(g->mf, adj + 0) : 0; +} + +u32 graph_read_edges(graph_t *g, u64 ent_off, adj_entry_t *out, u32 max) { + memfile_t *mf = g->mf; + u64 adj = rdu64(mf, ent_off + E_ADJ); + if (adj == 0) return 0; + u32 count = rdu32(mf, adj + 0); + u32 n = count < max ? count : max; + for (u32 i = 0; i < n; i++) { + u64 base = adj + ADJ_HEADER_SIZE + (u64)i * ADJ_ENTRY_SIZE; + u64 packed = rdu64(mf, base + AE_TARGET_DIR); + out[i].target_offset = packed >> 2; + out[i].direction = (u32)(packed & 3u); + out[i].rel_type_id = rdu32(mf, base + AE_RELTYPE); + out[i].mtime = rdu64(mf, base + AE_MTIME); + } + return count; +} + +/* ====================================================================== + * Operations + * ====================================================================== */ + +u64 graph_lookup(graph_t *g, const u8 *name, u16 name_len) { + u64 nid = st_find(g->st, name, name_len); + if (!nid) return 0; + return ni_lookup(g, (u32)nid); +} + +u64 graph_create_entity(graph_t *g, const u8 *name, u16 name_len, + const u8 *type, u16 type_len, u64 mtime) { + u64 existing = graph_lookup(g, name, name_len); + if (existing) return existing; /* dedup: no new refs */ + + u64 nid = st_intern(g->st, name, name_len); + u64 tid = st_intern(g->st, type, type_len); + u64 off = memfile_alloc(g->mf, ENTITY_RECORD_SIZE); + if (!off) return 0; + + memset(memfile_ptr(g->mf, off), 0, ENTITY_RECORD_SIZE); + wru32(g->mf, off + E_VERSION, ENTITY_CURRENT); + wru32(g->mf, off + E_NAME_ID, (u32)nid); + wru32(g->mf, off + E_TYPE_ID, (u32)tid); + wru64(g->mf, off + E_MTIME, mtime); + /* obs_mtime stays 0 until an observation is added (matches old no-obs => 0). */ + + log_append(g, off); + ni_insert(g, (u32)nid, off); + return off; +} + +int graph_delete_entity(graph_t *g, u64 off) { + entity_t e; + graph_read_entity(g, off, &e); + + /* edges: release every relType ref this entity's edges touch, drop mirrors */ + u32 ec = graph_edge_count(g, off); + if (ec) { + u32 cap = rdu32(g->mf, e.adj_offset + 4); + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, off, es, ec); + for (u32 k = 0; k < ec; k++) { + st_release(g->st, es[k].rel_type_id); /* this entity's own entry */ + if (es[k].target_offset != off) { /* not a self-loop */ + u32 rev = (es[k].direction == DIR_FORWARD) ? DIR_BACKWARD : DIR_FORWARD; + if (graph_remove_edge(g, es[k].target_offset, off, es[k].rel_type_id, rev)) + st_release(g->st, es[k].rel_type_id); /* neighbor's mirror entry */ + } + } + free(es); + memfile_free(g->mf, e.adj_offset, ADJ_HEADER_SIZE + (u64)cap * ADJ_ENTRY_SIZE); + } + + ni_remove(g, e.name_id); + log_remove(g, off); + + st_release(g->st, e.name_id); + st_release(g->st, e.type_id); + if (e.obs0_id) st_release(g->st, e.obs0_id); + if (e.obs1_id) st_release(g->st, e.obs1_id); + + memfile_free(g->mf, off, ENTITY_RECORD_SIZE); + return 1; +} + +int graph_create_relation(graph_t *g, u64 from, u64 to, const u8 *rt, u16 rt_len, u64 mtime) { + u64 rtid_f = st_intern(g->st, rt, rt_len); /* ref for the forward entry */ + adj_entry_t f = { to, DIR_FORWARD, (u32)rtid_f, mtime }; + graph_add_edge(g, from, &f); + + u64 rtid_b = st_intern(g->st, rt, rt_len); /* ref for the backward entry */ + adj_entry_t b = { from, DIR_BACKWARD, (u32)rtid_b, mtime }; + graph_add_edge(g, to, &b); + wru64(g->mf, from + E_MTIME, mtime); /* a new relation marks the source entity modified */ + return 1; +} + +int graph_delete_relation(graph_t *g, u64 from, u64 to, const u8 *rt, u16 rt_len) { + u64 rtid = st_find(g->st, rt, rt_len); + if (!rtid) return 0; + int removed = 0; + if (graph_remove_edge(g, from, to, (u32)rtid, DIR_FORWARD)) { st_release(g->st, rtid); removed = 1; } + if (graph_remove_edge(g, to, from, (u32)rtid, DIR_BACKWARD)) { st_release(g->st, rtid); removed = 1; } + return removed; +} + +u32 graph_entity_count(graph_t *g) { + return rdu32(g->mf, node_log_off(g) + 0); +} + +/* ====================================================================== + * Observations + * ====================================================================== */ + +int graph_add_observation(graph_t *g, u64 off, const u8 *obs, u16 len, u64 mtime) { + memfile_t *mf = g->mf; + u8 cnt = rdu8(mf, off + E_OBSCNT); + if (cnt >= 2) return 0; + u64 oid = st_intern(g->st, obs, len); + if (cnt == 0) wru32(mf, off + E_OBS0, (u32)oid); + else wru32(mf, off + E_OBS1, (u32)oid); + wru8(mf, off + E_OBSCNT, (u8)(cnt + 1)); + wru64(mf, off + E_OBSM, mtime); + wru64(mf, off + E_MTIME, mtime); + return 1; +} + +int graph_remove_observation(graph_t *g, u64 off, const u8 *obs, u16 len, u64 mtime) { + memfile_t *mf = g->mf; + u64 oid = st_find(g->st, obs, len); + if (!oid) return 0; + u32 o0 = rdu32(mf, off + E_OBS0), o1 = rdu32(mf, off + E_OBS1); + if (o0 == (u32)oid) { + st_release(g->st, o0); + wru32(mf, off + E_OBS0, o1); + wru32(mf, off + E_OBS1, 0); + } else if (o1 == (u32)oid) { + st_release(g->st, o1); + wru32(mf, off + E_OBS1, 0); + } else { + return 0; + } + wru8(mf, off + E_OBSCNT, (u8)(rdu8(mf, off + E_OBSCNT) - 1)); + wru64(mf, off + E_OBSM, mtime); + wru64(mf, off + E_MTIME, mtime); + return 1; +} + +/* ====================================================================== + * Scans / enumeration + * ====================================================================== */ + +const u8 *graph_entity_name(graph_t *g, u64 off, u16 *len_out) { + return st_get(g->st, rdu32(g->mf, off + E_NAME_ID), len_out); +} + +u32 graph_list_entities(graph_t *g, u64 *out, u32 max) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0); + u32 n = count < max ? count : max; + for (u32 i = 0; i < n; i++) out[i] = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + return count; +} + +u32 graph_entities_by_type(graph_t *g, const u8 *type, u16 len, u64 *out, u32 max) { + memfile_t *mf = g->mf; + u64 tid = st_find(g->st, type, len); + if (!tid) return 0; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0), found = 0; + for (u32 i = 0; i < count; i++) { + u64 e = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + if (rdu32(mf, e + E_TYPE_ID) == (u32)tid) { if (found < max) out[found] = e; found++; } + } + return found; +} + +u32 graph_orphaned(graph_t *g, u64 *out, u32 max) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0), found = 0; + for (u32 i = 0; i < count; i++) { + u64 e = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + if (graph_edge_count(g, e) == 0) { if (found < max) out[found] = e; found++; } + } + return found; +} + +u32 graph_relation_count(graph_t *g) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0); + u64 edges = 0; + for (u32 i = 0; i < count; i++) + edges += graph_edge_count(g, rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8)); + return (u32)(edges / 2); /* each relation = forward + backward entry */ +} + +static int cmp_u32(const void *a, const void *b) { + u32 x = *(const u32 *)a, y = *(const u32 *)b; + return (x > y) - (x < y); +} + +u32 graph_entity_types(graph_t *g, u32 *out, u32 max) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0); + if (count == 0) return 0; + u32 *tmp = malloc((size_t)count * sizeof(u32)); + for (u32 i = 0; i < count; i++) + tmp[i] = rdu32(mf, rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8) + E_TYPE_ID); + qsort(tmp, count, sizeof(u32), cmp_u32); + u32 distinct = 0; + for (u32 i = 0; i < count; i++) + if (i == 0 || tmp[i] != tmp[i - 1]) { if (distinct < max) out[distinct] = tmp[i]; distinct++; } + free(tmp); + return distinct; +} + +u32 graph_relation_types(graph_t *g, u32 *out, u32 max) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0); + u64 total = 0; + for (u32 i = 0; i < count; i++) + total += graph_edge_count(g, rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8)); + if (total == 0) return 0; + u32 *tmp = malloc((size_t)total * sizeof(u32)); + u64 k = 0; + for (u32 i = 0; i < count; i++) { + u64 e = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + u64 adj = rdu64(mf, e + E_ADJ); + if (!adj) continue; + u32 ec = rdu32(mf, adj + 0); + for (u32 j = 0; j < ec; j++) + tmp[k++] = rdu32(mf, adj + ADJ_HEADER_SIZE + (u64)j * ADJ_ENTRY_SIZE + AE_RELTYPE); + } + qsort(tmp, total, sizeof(u32), cmp_u32); + u32 distinct = 0; + for (u64 i = 0; i < total; i++) + if (i == 0 || tmp[i] != tmp[i - 1]) { if (distinct < max) out[distinct] = tmp[i]; distinct++; } + free(tmp); + return distinct; +} + +/* ====================================================================== + * Search (POSIX ERE over name + type + observations); full result set + * ====================================================================== */ + +static int match_id(graph_t *g, regex_t *re, u32 id) { + if (!id) return 0; + u16 len; const u8 *s = st_get(g->st, id, &len); + regmatch_t pm; pm.rm_so = 0; pm.rm_eo = (regoff_t)len; + return regexec(re, (const char *)s, 0, &pm, REG_STARTEND) == 0; +} + +u32 graph_search(graph_t *g, const char *pattern, u64 *out, u32 max) { + regex_t re; + if (regcomp(&re, pattern, REG_EXTENDED) != 0) return 0; /* POSIX ERE, case-sensitive; invalid pattern -> no matches */ + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 count = rdu32(mf, log + 0), found = 0; + for (u32 i = 0; i < count; i++) { + u64 e = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + if (match_id(g, &re, rdu32(mf, e + E_NAME_ID)) || + match_id(g, &re, rdu32(mf, e + E_TYPE_ID)) || + match_id(g, &re, rdu32(mf, e + E_OBS0)) || + match_id(g, &re, rdu32(mf, e + E_OBS1))) { + if (found < max) out[found] = e; + found++; + } + } + regfree(&re); + return found; +} + +/* ====================================================================== + * Traversal (BFS) — open-addressing u64->u64 map for visited / parent. + * Entity offsets are never 0 (records live above the header), so 0 = empty slot. + * ====================================================================== */ + +typedef struct { u64 *k, *v; u32 cap, cnt; } omap; + +static void omap_init(omap *m, u32 cap) { m->cap = cap; m->cnt = 0; m->k = calloc(cap, 8); m->v = calloc(cap, 8); } +static void omap_free(omap *m) { free(m->k); free(m->v); } +static inline u32 omap_h(u64 x, u32 cap) { return (u32)(((x * 0x9e3779b97f4a7c15ull) >> 32) % cap); } +static int omap_has(omap *m, u64 key) { + for (u32 i = omap_h(key, m->cap); m->k[i]; i = (i + 1) % m->cap) if (m->k[i] == key) return 1; + return 0; +} +static u64 omap_get(omap *m, u64 key) { + for (u32 i = omap_h(key, m->cap); m->k[i]; i = (i + 1) % m->cap) if (m->k[i] == key) return m->v[i]; + return 0; +} +static void omap_grow(omap *m) { + u32 nc = m->cap * 2; u64 *nk = calloc(nc, 8), *nv = calloc(nc, 8); + for (u32 j = 0; j < m->cap; j++) if (m->k[j]) { + u32 i = omap_h(m->k[j], nc); while (nk[i]) i = (i + 1) % nc; + nk[i] = m->k[j]; nv[i] = m->v[j]; + } + free(m->k); free(m->v); m->k = nk; m->v = nv; m->cap = nc; +} +static void omap_put(omap *m, u64 key, u64 val) { + if ((u64)(m->cnt + 1) * 10 > (u64)m->cap * 7) omap_grow(m); + u32 i = omap_h(key, m->cap); + while (m->k[i]) { if (m->k[i] == key) { m->v[i] = val; return; } i = (i + 1) % m->cap; } + m->k[i] = key; m->v[i] = val; m->cnt++; +} + +static inline int dir_match(u32 want, u32 have) { return want == DIR_ANY || have == want; } + +u32 graph_neighbors(graph_t *g, u64 start, u32 depth, u32 direction, u64 *out, u32 max) { + omap seen; omap_init(&seen, 256); + omap_put(&seen, start, 1); + u32 qcap = 256, head = 0, tail = 0; + u64 *q = malloc(qcap * 8); u32 *qd = malloc(qcap * 4); + q[tail] = start; qd[tail] = 0; tail++; + u32 found = 0; + while (head < tail) { + u64 f = q[head]; u32 d = qd[head]; head++; + if (d >= depth) continue; + u32 ec = graph_edge_count(g, f); + if (!ec) continue; + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, f, es, ec); + for (u32 k = 0; k < ec; k++) { + if (!dir_match(direction, es[k].direction)) continue; + u64 t = es[k].target_offset; + if (!omap_has(&seen, t)) { + omap_put(&seen, t, 1); + if (found < max) out[found] = t; + found++; + if (tail == qcap) { qcap *= 2; q = realloc(q, qcap * 8); qd = realloc(qd, qcap * 4); } + q[tail] = t; qd[tail] = d + 1; tail++; + } + } + free(es); + } + free(q); free(qd); omap_free(&seen); + return found; +} + +/* BFS shortest path with a best-effort fallback + byte budget (β-contract). + * + * - target_reached: BFS arrived at `to`. out_path = from..to. + * - else best-effort: out_path = from..farthest (deepest BFS node), so the + * caller has a retry anchor. farthest = 0 if no edge was expanded. + * - budget_exhausted: stopped because per-node bytes crossed budget_bytes. + * The target check fires BEFORE the budget check, so a discovery that IS + * the target succeeds even at budget 0. Per-node cost is the real C BFS + * bookkeeping (~28B: omap entry + queue) plus the name/relType bytes we + * touch — the same mechanism as the old JS budget, C-native sizing. + */ +u32 graph_find_path_ex(graph_t *g, u64 from, u64 to, u32 max_depth, u32 direction, + u64 budget_bytes, u64 *out_path, u32 max_path, + int *target_reached, int *budget_exhausted, u64 *farthest) { + *target_reached = 0; *budget_exhausted = 0; *farthest = 0; + if (from == to) { if (max_path >= 1) out_path[0] = from; *target_reached = 1; return 1; } + + omap parent; omap_init(&parent, 256); + omap_put(&parent, from, from); /* root sentinel */ + u32 qcap = 256, head = 0, tail = 0; + u64 *q = malloc(qcap * 8); u32 *qd = malloc(qcap * 4); + q[tail] = from; qd[tail] = 0; tail++; + + u16 fl; (void)graph_entity_name(g, from, &fl); + u64 bytes_used = (u64)fl + 28; + int found = 0, exhausted = 0; + + while (head < tail && !found && !exhausted) { + u64 f = q[head]; u32 d = qd[head]; head++; + if (d >= max_depth) continue; + u32 ec = graph_edge_count(g, f); + if (!ec) continue; + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, f, es, ec); + for (u32 k = 0; k < ec; k++) { + if (!dir_match(direction, es[k].direction)) continue; + u64 t = es[k].target_offset; + if (omap_has(&parent, t)) continue; + omap_put(&parent, t, f); + *farthest = t; + if (t == to) { found = 1; break; } /* target check first */ + u16 nl; (void)graph_entity_name(g, t, &nl); + u16 rl; (void)st_get(g->st, es[k].rel_type_id, &rl); + bytes_used += (u64)nl + (u64)rl + 28; + if (bytes_used >= budget_bytes) { exhausted = 1; break; } /* then budget */ + if (tail == qcap) { qcap *= 2; q = realloc(q, qcap * 8); qd = realloc(qd, qcap * 4); } + q[tail] = t; qd[tail] = d + 1; tail++; + } + free(es); + } + + u32 n = 0; + u64 endp = found ? to : *farthest; + if (endp) { + u64 *rev = malloc((size_t)max_path * 8); + for (u64 cur = endp; cur != from && n < max_path; cur = omap_get(&parent, cur)) rev[n++] = cur; + if (n < max_path) rev[n++] = from; + for (u32 i = 0; i < n; i++) out_path[i] = rev[n - 1 - i]; + free(rev); + } + free(q); free(qd); omap_free(&parent); + *target_reached = found; *budget_exhausted = exhausted; + return n; +} + +u32 graph_find_path(graph_t *g, u64 from, u64 to, u32 max_depth, u32 direction, + u64 *out_path, u32 max_path) { + int tr, be; u64 fa; + return graph_find_path_ex(g, from, to, max_depth, direction, (u64)-1, + out_path, max_path, &tr, &be, &fa); +} + +/* ====================================================================== + * Ranking: visit counting (pagerank/llmrank), MERW psi, random walk + * ====================================================================== */ + +static u64 g_rng = 0x9e3779b97f4a7c15ull; +void graph_seed_rng(u64 seed) { g_rng = seed ? seed : 0x9e3779b97f4a7c15ull; } +static inline u64 rng_u64(u64 *s) { u64 x = *s; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return *s = x; } +static inline double rng_d(u64 *s) { return (double)(rng_u64(s) >> 11) * (1.0 / 9007199254740992.0); } /* [0,1) */ + +void graph_inc_structural_visit(graph_t *g, u64 off) { + wru64(g->mf, off + E_SVIS, rdu64(g->mf, off + E_SVIS) + 1); + u64 hp = g->header_offset + GH_STRUCTURAL_TOTAL; + wru64(g->mf, hp, rdu64(g->mf, hp) + 1); +} +void graph_inc_walker_visit(graph_t *g, u64 off) { + wru64(g->mf, off + E_WVIS, rdu64(g->mf, off + E_WVIS) + 1); + u64 hp = g->header_offset + GH_WALKER_TOTAL; + wru64(g->mf, hp, rdu64(g->mf, hp) + 1); +} +u64 graph_structural_total(graph_t *g) { return rdu64(g->mf, g->header_offset + GH_STRUCTURAL_TOTAL); } +u64 graph_walker_total(graph_t *g) { return rdu64(g->mf, g->header_offset + GH_WALKER_TOTAL); } +double graph_structural_rank(graph_t *g, u64 off) { + u64 t = graph_structural_total(g); return t ? (double)rdu64(g->mf, off + E_SVIS) / (double)t : 0.0; +} +double graph_walker_rank(graph_t *g, u64 off) { + u64 t = graph_walker_total(g); return t ? (double)rdu64(g->mf, off + E_WVIS) / (double)t : 0.0; +} +double graph_get_psi(graph_t *g, u64 off) { return rdf64(g->mf, off + E_PSI); } + +/* migration: restore an entity's preserved fields exactly (logical rebuild). */ +void graph_set_entity_fields(graph_t *g, u64 off, u64 mtime, u64 obs_mtime, + u64 structural_visits, u64 walker_visits, double psi) { + wru64(g->mf, off + E_MTIME, mtime); + wru64(g->mf, off + E_OBSM, obs_mtime); + wru64(g->mf, off + E_SVIS, structural_visits); + wru64(g->mf, off + E_WVIS, walker_visits); + wrf64(g->mf, off + E_PSI, psi); +} +/* migration: restore the global visit totals. */ +void graph_set_totals(graph_t *g, u64 structural_total, u64 walker_total) { + wru64(g->mf, g->header_offset + GH_STRUCTURAL_TOTAL, structural_total); + wru64(g->mf, g->header_offset + GH_WALKER_TOTAL, walker_total); +} + +/* one MC complete-path walk (Avrachenkov Alg. 4): forward edges, damping, stop at dangling */ +static u32 structural_walk(graph_t *g, u64 start, double damping) { + u64 cur = start; u32 visits = 0; + for (;;) { + graph_inc_structural_visit(g, cur); visits++; + u32 ec = graph_edge_count(g, cur); + if (!ec) break; + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, cur, es, ec); + u32 fwd = 0; + for (u32 k = 0; k < ec; k++) if (es[k].direction == DIR_FORWARD) fwd++; + if (fwd == 0 || rng_d(&g_rng) >= damping) { free(es); break; } + u32 pick = (u32)(rng_d(&g_rng) * fwd); if (pick >= fwd) pick = fwd - 1; + u32 seen = 0; u64 next = cur; + for (u32 k = 0; k < ec; k++) if (es[k].direction == DIR_FORWARD) { if (seen == pick) { next = es[k].target_offset; break; } seen++; } + free(es); + cur = next; + } + return visits; +} + +u32 graph_structural_sample(graph_t *g, u32 iterations, double damping) { + u32 n = graph_entity_count(g); + if (n == 0) return 0; + u64 *offs = malloc((size_t)n * 8); + graph_list_entities(g, offs, n); + u32 total = 0; + for (u32 it = 0; it < iterations; it++) + for (u32 i = 0; i < n; i++) total += structural_walk(g, offs[i], damping); + free(offs); + return total; +} + +u32 graph_compute_merw_psi(graph_t *g, double alpha, u32 max_iter, double tol) { + u32 n = graph_entity_count(g); + if (n == 0) return 0; + u64 *offs = malloc((size_t)n * 8); + graph_list_entities(g, offs, n); + + omap idx; omap_init(&idx, n * 2 < 256 ? 256 : n * 2); + for (u32 i = 0; i < n; i++) omap_put(&idx, offs[i], i + 1); /* index+1; 0 = absent */ + + /* CSR forward adjacency */ + u32 *rowoff = malloc((size_t)(n + 1) * 4); + rowoff[0] = 0; + for (u32 i = 0; i < n; i++) { + u32 ec = graph_edge_count(g, offs[i]), d = 0; + if (ec) { + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, offs[i], es, ec); + for (u32 k = 0; k < ec; k++) if (es[k].direction == DIR_FORWARD && omap_get(&idx, es[k].target_offset)) d++; + free(es); + } + rowoff[i + 1] = rowoff[i] + d; + } + u32 nnz = rowoff[n]; + u32 *col = malloc((size_t)(nnz ? nnz : 1) * 4); + for (u32 i = 0; i < n; i++) { + u32 ec = graph_edge_count(g, offs[i]); + if (!ec) continue; + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, offs[i], es, ec); + u32 w = rowoff[i]; + for (u32 k = 0; k < ec; k++) if (es[k].direction == DIR_FORWARD) { + u64 j = omap_get(&idx, es[k].target_offset); + if (j) col[w++] = (u32)(j - 1); + } + free(es); + } + + double *psi = malloc((size_t)n * 8), *nx = malloc((size_t)n * 8); + double warm_sum = 0; u32 warm_cnt = 0; + for (u32 i = 0; i < n; i++) { double v = rdf64(g->mf, offs[i] + E_PSI); psi[i] = v; if (v > 0) { warm_sum += v; warm_cnt++; } } + if (warm_cnt) { double m = warm_sum / warm_cnt; for (u32 i = 0; i < n; i++) if (psi[i] <= 0) psi[i] = m; } + else { double u = 1.0 / __builtin_sqrt((double)n); for (u32 i = 0; i < n; i++) psi[i] = u; } + double nrm = 0; for (u32 i = 0; i < n; i++) nrm += psi[i] * psi[i]; nrm = __builtin_sqrt(nrm); + if (nrm > 0) for (u32 i = 0; i < n; i++) psi[i] /= nrm; + + double teleport = (1.0 - alpha) / (double)n; + u32 iter = 0; + for (iter = 0; iter < max_iter; iter++) { + for (u32 i = 0; i < n; i++) nx[i] = 0; + double psi_sum = 0; for (u32 i = 0; i < n; i++) psi_sum += psi[i]; + double tc = teleport * psi_sum; + for (u32 i = 0; i < n; i++) { double val = alpha * psi[i]; for (u32 p = rowoff[i]; p < rowoff[i + 1]; p++) nx[col[p]] += val; } + for (u32 i = 0; i < n; i++) nx[i] += tc; + double norm = 0; for (u32 i = 0; i < n; i++) norm += nx[i] * nx[i]; norm = __builtin_sqrt(norm); + if (norm > 0) for (u32 i = 0; i < n; i++) nx[i] /= norm; + double diff = 0; for (u32 i = 0; i < n; i++) { double d = nx[i] - psi[i]; diff += d * d; } diff = __builtin_sqrt(diff); + double *t = psi; psi = nx; nx = t; + if (diff < tol) { iter++; break; } + } + for (u32 i = 0; i < n; i++) { if (psi[i] < 0) psi[i] = 0; wrf64(g->mf, offs[i] + E_PSI, psi[i]); } + + free(offs); free(rowoff); free(col); free(psi); free(nx); omap_free(&idx); + return iter; +} + +u32 graph_random_walk(graph_t *g, u64 start, u32 depth, u32 direction, int merw_mode, + u64 seed, u64 *out_path, u32 max_path) { + u64 st = seed ? seed : g_rng; + u32 plen = 0; + if (max_path >= 1) out_path[plen] = start; + plen = 1; + u64 cur = start; + for (u32 i = 0; i < depth; i++) { + u32 ec = graph_edge_count(g, cur); + if (!ec) break; + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, cur, es, ec); + u64 *cand = malloc((size_t)ec * 8); double *cpsi = malloc((size_t)ec * 8); u32 nc = 0; + for (u32 k = 0; k < ec; k++) { + if (!dir_match(direction, es[k].direction)) continue; + u64 t = es[k].target_offset; if (t == cur) continue; + double p = rdf64(g->mf, t + E_PSI); + int found = 0; + for (u32 j = 0; j < nc; j++) if (cand[j] == t) { if (p > cpsi[j]) cpsi[j] = p; found = 1; break; } + if (!found) { cand[nc] = t; cpsi[nc] = p; nc++; } + } + free(es); + if (nc == 0) { free(cand); free(cpsi); break; } + double total_psi = 0; for (u32 j = 0; j < nc; j++) total_psi += cpsi[j]; + u64 chosen; + if (merw_mode && total_psi > 0) { + double r = rng_d(&st) * total_psi, cum = 0; chosen = cand[nc - 1]; + for (u32 j = 0; j < nc; j++) { cum += cpsi[j]; if (r <= cum) { chosen = cand[j]; break; } } + } else { + u32 ix = (u32)(rng_d(&st) * nc); if (ix >= nc) ix = nc - 1; chosen = cand[ix]; + } + free(cand); free(cpsi); + cur = chosen; + if (plen < max_path) out_path[plen] = cur; + plen++; + } + if (!seed) g_rng = st; + return plen; +} + +/* ====================================================================== + * validate_graph: integrity audit (observation limits + dangling edges) + * ====================================================================== */ + +u32 graph_validate_obs(graph_t *g, u64 *off, u8 *count, u8 *oversize, u32 max) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 n = rdu32(mf, log + 0), found = 0; + for (u32 i = 0; i < n; i++) { + u64 e = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + u8 oc = rdu8(mf, e + E_OBSCNT); + u32 o0 = rdu32(mf, e + E_OBS0), o1 = rdu32(mf, e + E_OBS1); + u8 ov = 0; u16 l; + if (o0) { (void)st_get(g->st, o0, &l); if (l > 140) ov |= 1; } + if (o1) { (void)st_get(g->st, o1, &l); if (l > 140) ov |= 2; } + if (oc > 2 || ov) { + if (found < max) { off[found] = e; count[found] = oc; oversize[found] = ov; } + found++; + } + } + return found; +} + +u32 graph_validate_dangling(graph_t *g, u64 *src, u64 *tgt, u32 max) { + memfile_t *mf = g->mf; + u64 log = node_log_off(g); + u32 n = rdu32(mf, log + 0); + omap live; omap_init(&live, n * 2 < 256 ? 256 : n * 2); + for (u32 i = 0; i < n; i++) + omap_put(&live, rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8), 1); + u32 found = 0; + for (u32 i = 0; i < n; i++) { + u64 e = rdu64(mf, log + NODE_LOG_HEADER_SIZE + (u64)i * 8); + u32 ec = graph_edge_count(g, e); + if (!ec) continue; + adj_entry_t *es = malloc((size_t)ec * sizeof(adj_entry_t)); + graph_read_edges(g, e, es, ec); + for (u32 k = 0; k < ec; k++) + if (!omap_has(&live, es[k].target_offset)) { + if (found < max) { src[found] = e; tgt[found] = es[k].target_offset; } + found++; + } + free(es); + } + omap_free(&live); + return found; +} + +/* ====================================================================== + * Lifecycle + * ====================================================================== */ + +static u64 graph_init(graph_t *g) { + memfile_t *mf = g->mf; + u64 hdr = memfile_alloc(mf, GRAPH_HEADER_SIZE); + u64 log = memfile_alloc(mf, NODE_LOG_HEADER_SIZE + (u64)INITIAL_LOG_CAPACITY * 8); + u64 ni_size = 8 + (u64)NI_INITIAL_BUCKETS * NI_BUCKET_SIZE; + u64 ni = memfile_alloc(mf, ni_size); + if (!hdr || !log || !ni) return 0; + + wru32(mf, log + 0, 0); + wru32(mf, log + 4, INITIAL_LOG_CAPACITY); + + memset(memfile_ptr(mf, ni), 0, ni_size); + wru32(mf, ni + 0, NI_INITIAL_BUCKETS); + wru32(mf, ni + 4, 0); + + memset(memfile_ptr(mf, hdr), 0, GRAPH_HEADER_SIZE); + wru64(mf, hdr + GH_NODE_LOG_OFF, log); + wru64(mf, hdr + GH_NAME_INDEX_OFF, ni); + wru32(mf, hdr + GH_SCHEMA_VERSION, GRAPH_SCHEMA_VERSION); + return hdr; +} + +graph_t *graph_open(const char *graph_path, stringtable_t *st, size_t initial_size) { + graph_t *g = calloc(1, sizeof(*g)); + if (!g) return NULL; + g->st = st; + g->mf = memfile_open(graph_path, initial_size ? initial_size : 65536); + if (!g->mf) { free(g); return NULL; } + + memfile_lock_exclusive(g->mf); + memfile_refresh(g->mf); + if (g->mf->header->allocated <= sizeof(memfile_header_t)) { + g->header_offset = graph_init(g); + memfile_sync(g->mf); + } else { + g->header_offset = sizeof(memfile_header_t); /* the file's first allocation */ + } + memfile_unlock(g->mf); + + if (g->header_offset == 0) { graph_close(g); return NULL; } + return g; +} + +void graph_sync(graph_t *g) { memfile_sync(g->mf); } + +void graph_close(graph_t *g) { + if (!g) return; + if (g->mf) { memfile_close(g->mf); free(g->mf); } + free(g); /* string table is shared; not closed here */ +} diff --git a/native/graph.h b/native/graph.h new file mode 100644 index 0000000..d38802d --- /dev/null +++ b/native/graph.h @@ -0,0 +1,137 @@ +/* + * Graph store over a v3 MemoryFile + a shared StringTable. + * + * Layouts are the v2 graph schema (ported verbatim from graphfile.ts): + * EntityRecord: 72 bytes (name_id, type_id, adj_offset, mtime, obsMtime, + * obs_count, obs0_id, obs1_id, structural/walker visits, psi) + * AdjEntry: 24 bytes (target<<2|dir, relType_id, mtime); bidirectional storage + * NodeLog: [count,capacity][u64 offsets...] + * + * v3 additions: + * - Graph header carries a PERSISTENT name index (name_id -> entity offset). + * - Graph SCHEMA version lives in the graph header, separate from the memfile + * FORMAT version (which memfile.c owns and pins to 3). + * + * Refcount discipline (string table): an adj entry owns ONE ref on its relType_id; + * an entity owns one ref each on name_id, type_id, and its observation ids. + */ +#ifndef GRAPH_H +#define GRAPH_H + +#include "memoryfile.h" +#include "stringtable.h" + +#define GRAPH_SCHEMA_VERSION 2u +#define ENTITY_RECORD_SIZE 76u /* [u32 version][72B body] — biscuit-style versioned record */ +#define ADJ_ENTRY_SIZE 24u +#define ADJ_HEADER_SIZE 8u +#define NODE_LOG_HEADER_SIZE 8u +#define INITIAL_ADJ_CAPACITY 4u +#define INITIAL_LOG_CAPACITY 256u +#define NI_INITIAL_BUCKETS 4096u + +/* direction (low 2 bits of target_and_dir) */ +#define DIR_FORWARD 0u +#define DIR_BACKWARD 1u +#define DIR_BIDIR 2u +#define DIR_ANY 255u /* traversal filter: follow edges of any direction */ + +typedef struct { + memfile_t *mf; /* graph file */ + stringtable_t *st; /* shared string table (not owned) */ + u64 header_offset; /* graph header block */ +} graph_t; + +typedef struct { + u64 offset; + u32 name_id, type_id; + u64 adj_offset, mtime, obs_mtime; + u8 obs_count; + u32 obs0_id, obs1_id; + u64 structural_visits, walker_visits; + double psi; +} entity_t; + +typedef struct { + u64 target_offset; + u32 direction; + u32 rel_type_id; + u64 mtime; +} adj_entry_t; + +/* lifecycle */ +graph_t *graph_open(const char *graph_path, stringtable_t *st, size_t initial_size); +void graph_close(graph_t *g); +void graph_sync(graph_t *g); + +/* entity ops */ +u64 graph_lookup(graph_t *g, const u8 *name, u16 name_len); /* entity offset, 0 if absent */ +u64 graph_create_entity(graph_t *g, const u8 *name, u16 name_len, + const u8 *type, u16 type_len, u64 mtime); /* offset (existing if dup) */ +int graph_delete_entity(graph_t *g, u64 offset); /* 1 if deleted, 0 if absent */ +void graph_read_entity(graph_t *g, u64 offset, entity_t *out); + +/* relation ops (bidirectional edges) */ +int graph_create_relation(graph_t *g, u64 from, u64 to, const u8 *rt, u16 rt_len, u64 mtime); +int graph_delete_relation(graph_t *g, u64 from, u64 to, const u8 *rt, u16 rt_len); + +/* adjacency primitives */ +void graph_add_edge(graph_t *g, u64 entity_off, const adj_entry_t *e); +int graph_remove_edge(graph_t *g, u64 entity_off, u64 target_off, u32 rel_type_id, u32 direction); +u32 graph_edge_count(graph_t *g, u64 entity_off); +/* read up to `max` edges into out[]; returns the true edge count (may exceed max). */ +u32 graph_read_edges(graph_t *g, u64 entity_off, adj_entry_t *out, u32 max); + +u32 graph_entity_count(graph_t *g); + +/* observations */ +int graph_add_observation(graph_t *g, u64 off, const u8 *obs, u16 len, u64 mtime); +int graph_remove_observation(graph_t *g, u64 off, const u8 *obs, u16 len, u64 mtime); + +/* scans / enumeration */ +const u8 *graph_entity_name(graph_t *g, u64 off, u16 *len_out); +u32 graph_list_entities(graph_t *g, u64 *out, u32 max); +u32 graph_entities_by_type(graph_t *g, const u8 *type, u16 len, u64 *out, u32 max); +u32 graph_orphaned(graph_t *g, u64 *out, u32 max); +u32 graph_relation_count(graph_t *g); +u32 graph_entity_types(graph_t *g, u32 *out, u32 max); /* distinct type ids */ +u32 graph_relation_types(graph_t *g, u32 *out, u32 max); /* distinct relType ids */ + +/* search: POSIX ERE over name + type + observations; returns all matches */ +u32 graph_search(graph_t *g, const char *pattern, u64 *out, u32 max); + +/* traversal */ +u32 graph_neighbors(graph_t *g, u64 start, u32 depth, u32 direction, u64 *out, u32 max); +u32 graph_find_path(graph_t *g, u64 from, u64 to, u32 max_depth, u32 direction, + u64 *out_path, u32 max_path); /* node count; 0 = no path */ +/* extended: best-effort path to farthest when target unreachable + byte budget. + * out params: target_reached, budget_exhausted, farthest (deepest BFS node, 0=none). */ +u32 graph_find_path_ex(graph_t *g, u64 from, u64 to, u32 max_depth, u32 direction, + u64 budget_bytes, u64 *out_path, u32 max_path, + int *target_reached, int *budget_exhausted, u64 *farthest); + +/* validate_graph: integrity audit */ +u32 graph_validate_obs(graph_t *g, u64 *off, u8 *count, u8 *oversize, u32 max); /* >2 obs or >140-byte obs */ +u32 graph_validate_dangling(graph_t *g, u64 *src, u64 *tgt, u32 max); /* edge target not a live entity */ + +/* ranking: visit counting (drives pagerank/llmrank), MERW psi, random walk */ +void graph_seed_rng(u64 seed); +void graph_inc_structural_visit(graph_t *g, u64 off); +void graph_inc_walker_visit(graph_t *g, u64 off); +u64 graph_structural_total(graph_t *g); +u64 graph_walker_total(graph_t *g); +double graph_structural_rank(graph_t *g, u64 off); /* structural_visits / total */ +double graph_walker_rank(graph_t *g, u64 off); /* walker_visits / total */ +double graph_get_psi(graph_t *g, u64 off); + +/* migration: restore preserved entity fields + global totals (logical rebuild). */ +void graph_set_entity_fields(graph_t *g, u64 off, u64 mtime, u64 obs_mtime, + u64 structural_visits, u64 walker_visits, double psi); +void graph_set_totals(graph_t *g, u64 structural_total, u64 walker_total); +u32 graph_structural_sample(graph_t *g, u32 iterations, double damping); /* MC pagerank; total visits */ +u32 graph_compute_merw_psi(graph_t *g, double alpha, u32 max_iter, double tol); /* iters run */ +/* random walk; mode: 1=merw (weighted by psi), 0=uniform; seed 0 = use global rng. Returns path node count. */ +u32 graph_random_walk(graph_t *g, u64 start, u32 depth, u32 direction, int merw_mode, + u64 seed, u64 *out_path, u32 max_path); + +#endif /* GRAPH_H */ diff --git a/native/graphbind.c b/native/graphbind.c new file mode 100644 index 0000000..fce8306 --- /dev/null +++ b/native/graphbind.c @@ -0,0 +1,321 @@ +/* + * N-API binding for the v3 graph store (stringtable + graph over memoryfile). + * Exposes the high-level C "DB" ops; the TS layer is a thin formatter/paginator. + * + * A Store handle wraps {stringtable_t*, graph_t*}. Offsets are passed as BigInt. + */ +#define NAPI_VERSION 8 +#include +#include +#include +#include +#include +#include +#include "stringtable.h" +#include "graph.h" + +typedef struct { stringtable_t *st; graph_t *g; } Store; + +#define NCALL(call) do { if ((call) != napi_ok) { napi_throw_error(env, NULL, "napi: " #call); return NULL; } } while (0) + +static napi_value mkU64(napi_env env, u64 v) { napi_value r; napi_create_bigint_uint64(env, v, &r); return r; } +static napi_value mkU32(napi_env env, u32 v) { napi_value r; napi_create_uint32(env, v, &r); return r; } +static napi_value mkF64(napi_env env, double v){ napi_value r; napi_create_double(env, v, &r); return r; } +static u64 getU64(napi_env env, napi_value v) { uint64_t r = 0; bool l; napi_get_value_bigint_uint64(env, v, &r, &l); return r; } +static u32 getU32(napi_env env, napi_value v) { uint32_t r = 0; napi_get_value_uint32(env, v, &r); return r; } +static double getF64(napi_env env, napi_value v){ double r = 0; napi_get_value_double(env, v, &r); return r; } + +/* read a JS string arg into buf, returns byte length */ +static u16 getStr(napi_env env, napi_value v, char *buf, size_t cap) { + size_t len = 0; + napi_get_value_string_utf8(env, v, buf, cap, &len); + return (u16)len; +} + +/* read a JS string arg into a freshly malloc'd buffer (no fixed-size truncation; + * names can be up to u16 length). Caller frees. */ +static char *getStrA(napi_env env, napi_value v, u16 *len_out) { + size_t len = 0; + napi_get_value_string_utf8(env, v, NULL, 0, &len); + char *buf = malloc(len + 1); + if (buf) napi_get_value_string_utf8(env, v, buf, len + 1, &len); + *len_out = (u16)len; + return buf; +} + +static Store *unwrap(napi_env env, napi_value v) { Store *s = NULL; napi_get_value_external(env, v, (void **)&s); return s; } + +static void store_finalize(napi_env env, void *data, void *hint) { + (void)env; (void)hint; + Store *s = (Store *)data; + if (s) { if (s->g) graph_close(s->g); if (s->st) st_close(s->st); free(s); } +} + +/* ---- args helper ---- */ +#define ARGS(n) size_t argc = (n); napi_value argv[(n)]; NCALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL)) +#define STORE Store *s = unwrap(env, argv[0]) + +/* ---- lifecycle ---- */ +static napi_value n_open(napi_env env, napi_callback_info info) { + ARGS(3); + char gp[4096], sp[4096]; + getStr(env, argv[0], gp, sizeof gp); + getStr(env, argv[1], sp, sizeof sp); + uint32_t initial = getU32(env, argv[2]); + Store *s = calloc(1, sizeof(Store)); + s->st = st_open(sp, initial); + s->g = s->st ? graph_open(gp, s->st, initial) : NULL; + if (!s->st || !s->g) { if (s->g) graph_close(s->g); if (s->st) st_close(s->st); free(s); + napi_throw_error(env, NULL, "graph store open failed"); return NULL; } + napi_value ext; NCALL(napi_create_external(env, s, store_finalize, NULL, &ext)); + return ext; +} +static napi_value n_close(napi_env env, napi_callback_info info) { ARGS(1); STORE; if (s) { if (s->g) graph_close(s->g); if (s->st) st_close(s->st); s->g = NULL; s->st = NULL; } return NULL; } +static napi_value n_sync(napi_env env, napi_callback_info info) { ARGS(1); STORE; graph_sync(s->g); st_sync(s->st); return NULL; } +static napi_value n_lock_sh(napi_env env, napi_callback_info info){ ARGS(1); STORE; memfile_lock_shared(s->g->mf); st_lock_shared(s->st); return NULL; } +static napi_value n_lock_ex(napi_env env, napi_callback_info info){ ARGS(1); STORE; memfile_lock_exclusive(s->g->mf); st_lock_exclusive(s->st); return NULL; } +static napi_value n_unlock(napi_env env, napi_callback_info info) { ARGS(1); STORE; st_unlock(s->st); memfile_unlock(s->g->mf); return NULL; } +static napi_value n_refresh(napi_env env, napi_callback_info info){ ARGS(1); STORE; memfile_refresh(s->g->mf); memfile_refresh(s->st->mf); return NULL; } + +/* ---- entity ops ---- */ +static napi_value n_lookup(napi_env env, napi_callback_info info) { + ARGS(2); STORE; u16 l; char *nm = getStrA(env, argv[1], &l); + napi_value r = mkU64(env, graph_lookup(s->g, (const u8 *)nm, l)); + free(nm); return r; +} +static napi_value n_create_entity(napi_env env, napi_callback_info info) { + ARGS(4); STORE; + u16 nl, tl; char *nm = getStrA(env, argv[1], &nl), *ty = getStrA(env, argv[2], &tl); + u64 mtime = getU64(env, argv[3]); + u64 off = graph_create_entity(s->g, (const u8 *)nm, nl, (const u8 *)ty, tl, mtime); + free(nm); free(ty); + return mkU64(env, off); +} +static napi_value n_delete_entity(napi_env env, napi_callback_info info) { + ARGS(2); STORE; napi_value r; napi_get_boolean(env, graph_delete_entity(s->g, getU64(env, argv[1])), &r); return r; +} +/* read_entity -> { name, type, observations[], mtime, obsMtime, structuralVisits, walkerVisits, psi } */ +static napi_value n_read_entity(napi_env env, napi_callback_info info) { + ARGS(2); STORE; u64 off = getU64(env, argv[1]); + entity_t e; graph_read_entity(s->g, off, &e); + napi_value o; NCALL(napi_create_object(env, &o)); + u16 l; const u8 *p; + p = st_get(s->st, e.name_id, &l); napi_value nm; napi_create_string_utf8(env, (const char *)p, l, &nm); napi_set_named_property(env, o, "name", nm); + p = st_get(s->st, e.type_id, &l); napi_value ty; napi_create_string_utf8(env, (const char *)p, l, &ty); napi_set_named_property(env, o, "type", ty); + napi_value obs; napi_create_array(env, &obs); u32 oi = 0; + if (e.obs0_id) { p = st_get(s->st, e.obs0_id, &l); napi_value s0; napi_create_string_utf8(env, (const char *)p, l, &s0); napi_set_element(env, obs, oi++, s0); } + if (e.obs1_id) { p = st_get(s->st, e.obs1_id, &l); napi_value s1; napi_create_string_utf8(env, (const char *)p, l, &s1); napi_set_element(env, obs, oi++, s1); } + napi_set_named_property(env, o, "observations", obs); + napi_set_named_property(env, o, "mtime", mkU64(env, e.mtime)); + napi_set_named_property(env, o, "obsMtime", mkU64(env, e.obs_mtime)); + napi_set_named_property(env, o, "structuralVisits", mkU64(env, e.structural_visits)); + napi_set_named_property(env, o, "walkerVisits", mkU64(env, e.walker_visits)); + napi_set_named_property(env, o, "psi", mkF64(env, e.psi)); + return o; +} +static napi_value n_add_obs(napi_env env, napi_callback_info info) { + ARGS(4); STORE; char ob[4096]; u16 l = getStr(env, argv[2], ob, sizeof ob); + napi_value r; napi_get_boolean(env, graph_add_observation(s->g, getU64(env, argv[1]), (const u8 *)ob, l, getU64(env, argv[3])), &r); return r; +} +static napi_value n_remove_obs(napi_env env, napi_callback_info info) { + ARGS(4); STORE; char ob[4096]; u16 l = getStr(env, argv[2], ob, sizeof ob); + napi_value r; napi_get_boolean(env, graph_remove_observation(s->g, getU64(env, argv[1]), (const u8 *)ob, l, getU64(env, argv[3])), &r); return r; +} + +/* ---- relations ---- */ +static napi_value n_create_relation(napi_env env, napi_callback_info info) { + ARGS(5); STORE; char rt[4096]; u16 l = getStr(env, argv[3], rt, sizeof rt); + graph_create_relation(s->g, getU64(env, argv[1]), getU64(env, argv[2]), (const u8 *)rt, l, getU64(env, argv[4])); + return NULL; +} +static napi_value n_delete_relation(napi_env env, napi_callback_info info) { + ARGS(4); STORE; char rt[4096]; u16 l = getStr(env, argv[3], rt, sizeof rt); + napi_value r; napi_get_boolean(env, graph_delete_relation(s->g, getU64(env, argv[1]), getU64(env, argv[2]), (const u8 *)rt, l), &r); return r; +} +/* edges(off) -> [{ target, direction, relType, mtime }] */ +static napi_value n_edges(napi_env env, napi_callback_info info) { + ARGS(2); STORE; u64 off = getU64(env, argv[1]); + u32 ec = graph_edge_count(s->g, off); + adj_entry_t *es = malloc((ec ? ec : 1) * sizeof(adj_entry_t)); + graph_read_edges(s->g, off, es, ec); + napi_value arr; napi_create_array(env, &arr); + for (u32 i = 0; i < ec; i++) { + napi_value o; napi_create_object(env, &o); + napi_set_named_property(env, o, "target", mkU64(env, es[i].target_offset)); + napi_set_named_property(env, o, "direction", mkU32(env, es[i].direction)); + u16 l; const u8 *p = st_get(s->st, es[i].rel_type_id, &l); + napi_value rt; napi_create_string_utf8(env, (const char *)p, l, &rt); + napi_set_named_property(env, o, "relType", rt); + napi_set_named_property(env, o, "mtime", mkU64(env, es[i].mtime)); + napi_set_element(env, arr, i, o); + } + free(es); + return arr; +} + +/* ---- helper: return a u64[] result via a count-returning op into a BigInt array ---- */ +static napi_value u64arr(napi_env env, u64 *buf, u32 n) { + napi_value arr; napi_create_array(env, &arr); + for (u32 i = 0; i < n; i++) napi_set_element(env, arr, i, mkU64(env, buf[i])); + return arr; +} + +/* ---- traversal / search / scans (full result sets; TS paginates) ---- */ +static napi_value n_neighbors(napi_env env, napi_callback_info info) { + ARGS(4); STORE; u32 cap = graph_entity_count(s->g) + 1; u64 *out = malloc((size_t)cap * 8); + u32 n = graph_neighbors(s->g, getU64(env, argv[1]), getU32(env, argv[2]), getU32(env, argv[3]), out, cap); + napi_value r = u64arr(env, out, n < cap ? n : cap); free(out); return r; +} +/* find_path(h, from, to, maxDepth, direction, budgetBytes) + * -> { path:[offset], targetReached, budgetExhausted, farthest } */ +static napi_value n_find_path(napi_env env, napi_callback_info info) { + ARGS(6); STORE; u32 cap = graph_entity_count(s->g) + 2; u64 *out = malloc((size_t)cap * 8); + int tr = 0, be = 0; u64 fa = 0; + u32 n = graph_find_path_ex(s->g, getU64(env, argv[1]), getU64(env, argv[2]), getU32(env, argv[3]), + getU32(env, argv[4]), getU64(env, argv[5]), out, cap, &tr, &be, &fa); + if (n > cap) n = cap; + napi_value o; napi_create_object(env, &o); + napi_set_named_property(env, o, "path", u64arr(env, out, n)); + napi_value b1, b2; napi_get_boolean(env, tr, &b1); napi_get_boolean(env, be, &b2); + napi_set_named_property(env, o, "targetReached", b1); + napi_set_named_property(env, o, "budgetExhausted", b2); + napi_set_named_property(env, o, "farthest", mkU64(env, fa)); + free(out); return o; +} +static napi_value n_search(napi_env env, napi_callback_info info) { + ARGS(2); STORE; char pat[8192]; getStr(env, argv[1], pat, sizeof pat); + u32 cap = graph_entity_count(s->g) + 1; u64 *out = malloc((size_t)cap * 8); + u32 n = graph_search(s->g, pat, out, cap); + napi_value r = u64arr(env, out, n < cap ? n : cap); free(out); return r; +} +static napi_value n_by_type(napi_env env, napi_callback_info info) { + ARGS(2); STORE; char ty[4096]; u16 l = getStr(env, argv[1], ty, sizeof ty); + u32 cap = graph_entity_count(s->g) + 1; u64 *out = malloc((size_t)cap * 8); + u32 n = graph_entities_by_type(s->g, (const u8 *)ty, l, out, cap); + napi_value r = u64arr(env, out, n < cap ? n : cap); free(out); return r; +} +static napi_value n_orphaned(napi_env env, napi_callback_info info) { + ARGS(1); STORE; u32 cap = graph_entity_count(s->g) + 1; u64 *out = malloc((size_t)cap * 8); + u32 n = graph_orphaned(s->g, out, cap); + napi_value r = u64arr(env, out, n < cap ? n : cap); free(out); return r; +} +static napi_value n_list_entities(napi_env env, napi_callback_info info) { + ARGS(1); STORE; u32 cap = graph_entity_count(s->g) + 1; u64 *out = malloc((size_t)cap * 8); + u32 n = graph_list_entities(s->g, out, cap); + napi_value r = u64arr(env, out, n < cap ? n : cap); free(out); return r; +} +/* distinct type ids -> [string] */ +static napi_value n_str_of_ids(napi_env env, Store *s, u32 *ids, u32 n) { + napi_value arr; napi_create_array(env, &arr); + for (u32 i = 0; i < n; i++) { u16 l; const u8 *p = st_get(s->st, ids[i], &l); napi_value v; napi_create_string_utf8(env, (const char *)p, l, &v); napi_set_element(env, arr, i, v); } + return arr; +} +static napi_value n_entity_types(napi_env env, napi_callback_info info) { + ARGS(1); STORE; u32 cap = graph_entity_count(s->g) + 1; u32 *out = malloc((size_t)cap * 4); + u32 n = graph_entity_types(s->g, out, cap); napi_value r = n_str_of_ids(env, s, out, n < cap ? n : cap); free(out); return r; +} +static napi_value n_relation_types(napi_env env, napi_callback_info info) { + ARGS(1); STORE; u32 cap = graph_relation_count(s->g) * 2 + 8; u32 *out = malloc((size_t)cap * 4); + u32 n = graph_relation_types(s->g, out, cap); napi_value r = n_str_of_ids(env, s, out, n < cap ? n : cap); free(out); return r; +} +static napi_value n_entity_count(napi_env env, napi_callback_info info) { ARGS(1); STORE; return mkU32(env, graph_entity_count(s->g)); } +static napi_value n_relation_count(napi_env env, napi_callback_info info){ ARGS(1); STORE; return mkU32(env, graph_relation_count(s->g)); } +static napi_value n_entity_name(napi_env env, napi_callback_info info) { + ARGS(2); STORE; u16 l; const u8 *p = graph_entity_name(s->g, getU64(env, argv[1]), &l); + napi_value v; napi_create_string_utf8(env, (const char *)p, l, &v); return v; +} + +/* ---- ranking ---- */ +static napi_value n_inc_walker(napi_env env, napi_callback_info info) { ARGS(2); STORE; graph_inc_walker_visit(s->g, getU64(env, argv[1])); return NULL; } +static napi_value n_inc_structural(napi_env env, napi_callback_info info) { ARGS(2); STORE; graph_inc_structural_visit(s->g, getU64(env, argv[1])); return NULL; } +static napi_value n_structural_total(napi_env env, napi_callback_info info){ ARGS(1); STORE; return mkU64(env, graph_structural_total(s->g)); } +static napi_value n_walker_total(napi_env env, napi_callback_info info) { ARGS(1); STORE; return mkU64(env, graph_walker_total(s->g)); } +static napi_value n_structural_rank(napi_env env, napi_callback_info info) { ARGS(2); STORE; return mkF64(env, graph_structural_rank(s->g, getU64(env, argv[1]))); } +static napi_value n_walker_rank(napi_env env, napi_callback_info info) { ARGS(2); STORE; return mkF64(env, graph_walker_rank(s->g, getU64(env, argv[1]))); } +static napi_value n_get_psi(napi_env env, napi_callback_info info) { ARGS(2); STORE; return mkF64(env, graph_get_psi(s->g, getU64(env, argv[1]))); } +static napi_value n_structural_sample(napi_env env, napi_callback_info info){ ARGS(3); STORE; return mkU32(env, graph_structural_sample(s->g, getU32(env, argv[1]), getF64(env, argv[2]))); } +static napi_value n_merw(napi_env env, napi_callback_info info) { ARGS(4); STORE; return mkU32(env, graph_compute_merw_psi(s->g, getF64(env, argv[1]), getU32(env, argv[2]), getF64(env, argv[3]))); } +static napi_value n_seed(napi_env env, napi_callback_info info) { ARGS(2); (void)unwrap(env, argv[0]); graph_seed_rng(getU64(env, argv[1])); return NULL; } +static napi_value n_random_walk(napi_env env, napi_callback_info info) { + ARGS(6); STORE; u32 depth = getU32(env, argv[2]); u32 cap = depth + 1; u64 *out = malloc((size_t)cap * 8); + u32 n = graph_random_walk(s->g, getU64(env, argv[1]), depth, getU32(env, argv[3]), getU32(env, argv[4]), getU64(env, argv[5]), out, cap); + napi_value r = u64arr(env, out, n < cap ? n : cap); free(out); return r; +} + +/* ---- validate ---- */ +static napi_value n_validate_obs(napi_env env, napi_callback_info info) { + ARGS(1); STORE; u32 cap = graph_entity_count(s->g) + 1; + u64 *off = malloc((size_t)cap * 8); u8 *cnt = malloc(cap), *ov = malloc(cap); + u32 n = graph_validate_obs(s->g, off, cnt, ov, cap); if (n > cap) n = cap; + napi_value arr; napi_create_array(env, &arr); + for (u32 i = 0; i < n; i++) { napi_value o; napi_create_object(env, &o); + napi_set_named_property(env, o, "offset", mkU64(env, off[i])); + napi_set_named_property(env, o, "count", mkU32(env, cnt[i])); + napi_set_named_property(env, o, "oversize", mkU32(env, ov[i])); + napi_set_element(env, arr, i, o); } + free(off); free(cnt); free(ov); return arr; +} +static napi_value n_validate_dangling(napi_env env, napi_callback_info info) { + ARGS(1); STORE; u32 cap = graph_entity_count(s->g) + 1; + u64 *src = malloc((size_t)cap * 8), *tgt = malloc((size_t)cap * 8); + u32 n = graph_validate_dangling(s->g, src, tgt, cap); if (n > cap) n = cap; + napi_value arr; napi_create_array(env, &arr); + for (u32 i = 0; i < n; i++) { napi_value o; napi_create_object(env, &o); + napi_set_named_property(env, o, "src", mkU64(env, src[i])); + napi_set_named_property(env, o, "target", mkU64(env, tgt[i])); + napi_set_element(env, arr, i, o); } + free(src); free(tgt); return arr; +} + +/* ---- migration setters (restore preserved fields after logical rebuild) ---- */ +static napi_value n_set_entity_fields(napi_env env, napi_callback_info info) { + ARGS(7); STORE; + graph_set_entity_fields(s->g, getU64(env, argv[1]), getU64(env, argv[2]), getU64(env, argv[3]), + getU64(env, argv[4]), getU64(env, argv[5]), getF64(env, argv[6])); + return NULL; +} +static napi_value n_set_totals(napi_env env, napi_callback_info info) { + ARGS(3); STORE; + graph_set_totals(s->g, getU64(env, argv[1]), getU64(env, argv[2])); + return NULL; +} + +/* ---- migration serialization: kernel flock on a lock file. Blocks until held; + * auto-released on process death (no stale locks). Not Store-bound. ---- */ +static napi_value n_lock_path(napi_env env, napi_callback_info info) { + ARGS(1); + char p[4096]; getStr(env, argv[0], p, sizeof p); + int fd = open(p, O_CREAT | O_RDWR, 0644); + if (fd >= 0 && flock(fd, LOCK_EX) != 0) { close(fd); fd = -1; } + return mkU32(env, (u32)fd); +} +static napi_value n_unlock_path(napi_env env, napi_callback_info info) { + ARGS(1); + int fd = (int)getU32(env, argv[0]); + if (fd >= 0) { flock(fd, LOCK_UN); close(fd); } + return NULL; +} + +#define EXPORT(name, fn) do { napi_value f; napi_create_function(env, name, NAPI_AUTO_LENGTH, fn, NULL, &f); napi_set_named_property(env, exports, name, f); } while (0) + +NAPI_MODULE_INIT() { + EXPORT("open", n_open); EXPORT("close", n_close); EXPORT("sync", n_sync); + EXPORT("lockShared", n_lock_sh); EXPORT("lockExclusive", n_lock_ex); EXPORT("unlock", n_unlock); EXPORT("refresh", n_refresh); + EXPORT("lookup", n_lookup); EXPORT("createEntity", n_create_entity); EXPORT("deleteEntity", n_delete_entity); + EXPORT("readEntity", n_read_entity); EXPORT("entityName", n_entity_name); + EXPORT("addObservation", n_add_obs); EXPORT("removeObservation", n_remove_obs); + EXPORT("createRelation", n_create_relation); EXPORT("deleteRelation", n_delete_relation); EXPORT("edges", n_edges); + EXPORT("neighbors", n_neighbors); EXPORT("findPath", n_find_path); EXPORT("search", n_search); + EXPORT("entitiesByType", n_by_type); EXPORT("orphaned", n_orphaned); EXPORT("listEntities", n_list_entities); + EXPORT("entityTypes", n_entity_types); EXPORT("relationTypes", n_relation_types); + EXPORT("entityCount", n_entity_count); EXPORT("relationCount", n_relation_count); + EXPORT("incWalkerVisit", n_inc_walker); EXPORT("incStructuralVisit", n_inc_structural); + EXPORT("structuralTotal", n_structural_total); EXPORT("walkerTotal", n_walker_total); + EXPORT("structuralRank", n_structural_rank); EXPORT("walkerRank", n_walker_rank); EXPORT("getPsi", n_get_psi); + EXPORT("structuralSample", n_structural_sample); EXPORT("computeMerwPsi", n_merw); EXPORT("seedRng", n_seed); + EXPORT("randomWalk", n_random_walk); + EXPORT("validateObs", n_validate_obs); EXPORT("validateDangling", n_validate_dangling); + EXPORT("setEntityFields", n_set_entity_fields); EXPORT("setTotals", n_set_totals); + EXPORT("lockPath", n_lock_path); EXPORT("unlockPath", n_unlock_path); + return exports; +} diff --git a/native/memoryfile.c b/native/memoryfile.c index c2644d0..fa551b0 100644 --- a/native/memoryfile.c +++ b/native/memoryfile.c @@ -1,11 +1,15 @@ /* - * Memory File - mmap-based arena allocator + * Memory File v3 - mmap-backed arena, Cartesian-tree free allocator. * - * Originally from biscuit/server; adapted for MCP memory server. - * Added: flock-based concurrency, read/write helpers. + * Substrate (file/mmap/growth/locks) originally from biscuit/server. + * Allocator: Stephenson "Fast Fits" Cartesian tree resident in the free blocks. + * - BST keyed by ADDRESS (a node's own offset) => address-neighbours for coalescing + * - max-HEAP keyed by SIZE => O(h) lowest-address fit + * Live blocks carry zero metadata; deallocation is sized; coalescing is continuous. */ #include "memoryfile.h" + #include #include #include @@ -14,13 +18,31 @@ #include #include +#ifdef MEMFILE_DOUBLE_FREE_CHECK +#include /* test-only: double-free detector diagnostics */ +#endif + +#define MFC_MIN_BLOCK 32u /* sizeof {u64 size, left, right, parent} */ +#define MFC_QUANTUM 32u /* alloc granularity == min block: split leftovers stay valid */ + +/* Cartesian-tree node, resident in a free block's own bytes. */ +typedef struct __attribute__((packed)) { u64 size, left, right, parent; } mf_node_t; + /* ========================================================================= * Pointer conversion * ========================================================================= */ void *memfile_ptr(memfile_t *mf, u64 offset) { if (offset == 0 || offset >= mf->mmap_size) return NULL; - return (u8*)mf->mmap_base + offset; + return (u8 *)mf->mmap_base + offset; +} + +/* Fast internal accessor (offsets are valid by construction in the allocator). */ +static inline mf_node_t *ND(memfile_t *mf, u64 o) { return (mf_node_t *)((u8 *)mf->mmap_base + o); } + +static inline u64 round_up32(u64 s) { + s = (s + (MFC_QUANTUM - 1)) & ~(u64)(MFC_QUANTUM - 1); + return s < MFC_MIN_BLOCK ? MFC_MIN_BLOCK : s; } /* ========================================================================= @@ -48,195 +70,162 @@ int memfile_write(memfile_t *mf, u64 offset, const void *buf, u64 len) { * ========================================================================= */ static int memfile_remap(memfile_t *mf, size_t new_size) { - if (ftruncate(mf->fd, new_size) < 0) { - return -1; - } - + if (ftruncate(mf->fd, new_size) < 0) return -1; #ifdef __linux__ void *new_base = mremap(mf->mmap_base, mf->mmap_size, new_size, MREMAP_MAYMOVE); - if (new_base == MAP_FAILED) { - return -1; - } + if (new_base == MAP_FAILED) return -1; #else - /* Portable fallback: unmap old region, map new size */ munmap(mf->mmap_base, mf->mmap_size); - void *new_base = mmap(NULL, new_size, PROT_READ | PROT_WRITE, - MAP_SHARED, mf->fd, 0); - if (new_base == MAP_FAILED) { - return -1; - } + void *new_base = mmap(NULL, new_size, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); + if (new_base == MAP_FAILED) return -1; #endif - mf->mmap_base = new_base; mf->mmap_size = new_size; - mf->header = (memfile_header_t*)new_base; + mf->header = (memfile_header_t *)new_base; mf->header->file_size = new_size; - return 0; } static int memfile_ensure_space(memfile_t *mf, u64 needed) { - if (mf->header->allocated + needed <= mf->header->file_size) { - return 0; - } - + if (mf->header->allocated + needed <= mf->header->file_size) return 0; size_t new_size = mf->mmap_size * 2; - if (new_size < mf->header->allocated + needed) { - new_size = mf->header->allocated + needed + 4096; - } - + if (new_size < mf->header->allocated + needed) new_size = mf->header->allocated + needed + 4096; return memfile_remap(mf, new_size); } /* ========================================================================= - * Allocation + * Cartesian-tree allocator * ========================================================================= */ -u64 memfile_alloc(memfile_t *mf, u64 size) { - u64 total_size = size + sizeof(memfile_alloc_t); - - /* Align to 8 bytes */ - total_size = (total_size + 7) & ~7ULL; - - /* Try free list first (first-fit) */ - u64 prev_offset = 0; - u64 free_offset = mf->header->free_list_head; - - while (free_offset != 0) { - memfile_free_t *free_block = (memfile_free_t*)memfile_ptr(mf, free_offset); - - if (free_block->size >= total_size) { - u64 remaining = free_block->size - total_size; - - if (remaining >= sizeof(memfile_free_t) + 8) { - /* Split the block */ - u64 new_free_offset = free_offset + total_size; - memfile_free_t *new_free = (memfile_free_t*)memfile_ptr(mf, new_free_offset); - new_free->size = remaining; - new_free->next = free_block->next; - - if (prev_offset == 0) { - mf->header->free_list_head = new_free_offset; - } else { - memfile_free_t *prev = (memfile_free_t*)memfile_ptr(mf, prev_offset); - prev->next = new_free_offset; - } - } else { - /* Use entire block (avoid tiny leftover) */ - total_size = free_block->size; - - if (prev_offset == 0) { - mf->header->free_list_head = free_block->next; - } else { - memfile_free_t *prev = (memfile_free_t*)memfile_ptr(mf, prev_offset); - prev->next = free_block->next; - } - } - - memfile_alloc_t *alloc = (memfile_alloc_t*)memfile_ptr(mf, free_offset); - alloc->size = total_size; - - return free_offset + sizeof(memfile_alloc_t); - } - - prev_offset = free_offset; - free_offset = free_block->next; - } - - /* No suitable free block - bump allocate from end */ - if (memfile_ensure_space(mf, total_size) < 0) { - return 0; +/* Rotate child x above its parent, preserving BST order; fix parent links + root. */ +static void mf_rot(memfile_t *mf, u64 x) { + u64 p = ND(mf, x)->parent; + u64 g = ND(mf, p)->parent; + if (ND(mf, p)->left == x) { /* right rotation */ + u64 b = ND(mf, x)->right; + ND(mf, x)->right = p; ND(mf, p)->parent = x; + ND(mf, p)->left = b; if (b) ND(mf, b)->parent = p; + } else { /* left rotation */ + u64 b = ND(mf, x)->left; + ND(mf, x)->left = p; ND(mf, p)->parent = x; + ND(mf, p)->right = b; if (b) ND(mf, b)->parent = p; } - - u64 offset = mf->header->allocated; - memfile_alloc_t *alloc = (memfile_alloc_t*)memfile_ptr(mf, offset); - alloc->size = total_size; - mf->header->allocated += total_size; - - return offset + sizeof(memfile_alloc_t); + ND(mf, x)->parent = g; + if (!g) mf->header->free_root = x; + else if (ND(mf, g)->left == p) ND(mf, g)->left = x; else ND(mf, g)->right = x; } -void memfile_free(memfile_t *mf, u64 offset) { - if (offset == 0) return; +static void mf_insert(memfile_t *mf, u64 off, u64 size) { + ND(mf, off)->size = size; ND(mf, off)->left = 0; ND(mf, off)->right = 0; ND(mf, off)->parent = 0; + mf->header->free_bytes += size; mf->header->free_count += 1; - u64 alloc_offset = offset - sizeof(memfile_alloc_t); - memfile_alloc_t *alloc = (memfile_alloc_t*)memfile_ptr(mf, alloc_offset); + u64 root = mf->header->free_root; + if (!root) { mf->header->free_root = off; return; } - /* Free list node lives in the freed space itself */ - memfile_free_t *free_block = (memfile_free_t*)alloc; - free_block->size = alloc->size; - free_block->next = mf->header->free_list_head; + u64 cur = root, par = 0; + while (cur) { par = cur; cur = (off < cur) ? ND(mf, cur)->left : ND(mf, cur)->right; } + ND(mf, off)->parent = par; + if (off < par) ND(mf, par)->left = off; else ND(mf, par)->right = off; - mf->header->free_list_head = alloc_offset; + while (ND(mf, off)->parent && ND(mf, ND(mf, off)->parent)->size < ND(mf, off)->size) + mf_rot(mf, off); } -/* ========================================================================= - * Coalescing - merge adjacent free blocks - * ========================================================================= */ - -void memfile_coalesce(memfile_t *mf) { - if (mf->header->free_list_head == 0) return; - - /* Count free blocks */ - u32 free_count = 0; - u64 offset = mf->header->free_list_head; - while (offset != 0) { - free_count++; - memfile_free_t *block = (memfile_free_t*)memfile_ptr(mf, offset); - offset = block->next; +static void mf_remove(memfile_t *mf, u64 off) { + for (;;) { + u64 l = ND(mf, off)->left, r = ND(mf, off)->right; + if (!l && !r) break; + u64 bigger = !l ? r : !r ? l : (ND(mf, l)->size >= ND(mf, r)->size ? l : r); + mf_rot(mf, bigger); } + u64 p = ND(mf, off)->parent; + if (!p) mf->header->free_root = 0; + else if (ND(mf, p)->left == off) ND(mf, p)->left = 0; else ND(mf, p)->right = 0; + mf->header->free_bytes -= ND(mf, off)->size; mf->header->free_count -= 1; +} - if (free_count < 2) return; +/* Lowest-address free block with size >= n, in O(h). */ +static u64 mf_fit(memfile_t *mf, u64 n) { + u64 cur = mf->header->free_root; + while (cur) { + u64 l = ND(mf, cur)->left; + if (l && ND(mf, l)->size >= n) { cur = l; continue; } + if (ND(mf, cur)->size >= n) return cur; + cur = ND(mf, cur)->right; + } + return 0; +} - /* Collect into temp array */ - struct { u64 offset; u64 size; } *blocks = malloc(free_count * sizeof(*blocks)); +static u64 mf_succ(memfile_t *mf, u64 off) { /* smallest address > off */ + u64 cur = mf->header->free_root, s = 0; + while (cur) { if (cur > off) { s = cur; cur = ND(mf, cur)->left; } else cur = ND(mf, cur)->right; } + return s; +} +static u64 mf_pred(memfile_t *mf, u64 off) { /* largest address < off */ + u64 cur = mf->header->free_root, p = 0; + while (cur) { if (cur < off) { p = cur; cur = ND(mf, cur)->right; } else cur = ND(mf, cur)->left; } + return p; +} - offset = mf->header->free_list_head; - for (u32 i = 0; i < free_count; i++) { - memfile_free_t *block = (memfile_free_t*)memfile_ptr(mf, offset); - blocks[i].offset = offset; - blocks[i].size = block->size; - offset = block->next; +u64 memfile_alloc(memfile_t *mf, u64 size) { + u64 n = round_up32(size); + u64 b = mf_fit(mf, n); + if (b) { + u64 bsz = ND(mf, b)->size; + mf_remove(mf, b); + u64 rem = bsz - n; /* 0 or >= 32 */ + if (rem >= MFC_MIN_BLOCK) mf_insert(mf, b + n, rem); + return b; } + if (memfile_ensure_space(mf, n) < 0) return 0; + u64 off = mf->header->allocated; + mf->header->allocated += n; + return off; +} - /* Sort by offset (insertion sort - fine for expected small N) */ - for (u32 i = 1; i < free_count; i++) { - u64 key_off = blocks[i].offset; - u64 key_size = blocks[i].size; - int j = i - 1; - while (j >= 0 && blocks[j].offset > key_off) { - blocks[j + 1] = blocks[j]; - j--; - } - blocks[j + 1].offset = key_off; - blocks[j + 1].size = key_size; +#ifdef MEMFILE_DOUBLE_FREE_CHECK +/* Test-only double-free / overlapping-free detector. NEVER compiled into the + * published binary (binding.gyp does not define MEMFILE_DOUBLE_FREE_CHECK), so + * the production allocator stays clean (Principle_FixCauseNotChecks). A valid + * free targets an entirely ALLOCATED range, so it must intersect no free block. */ +static u64 mf_node_at(memfile_t *mf, u64 off) { + u64 cur = mf->header->free_root; + while (cur) { if (cur == off) return cur; cur = (off < cur) ? ND(mf, cur)->left : ND(mf, cur)->right; } + return 0; +} +static void mf_assert_allocated(memfile_t *mf, u64 offset, u64 n) { + const char *why = 0; u64 p, s; + if (mf_node_at(mf, offset)) why = "offset is already a free block"; + else if ((p = mf_pred(mf, offset)) && p + ND(mf, p)->size > offset) why = "range intersects a preceding free block"; + else if ((s = mf_succ(mf, offset)) && offset + n > s) why = "range intersects a following free block"; + if (why) { + fprintf(stderr, "\n*** memfile DOUBLE-FREE detected: offset=%llu size=%llu (%s) ***\n", + (unsigned long long)offset, (unsigned long long)n, why); + abort(); } +} +#endif - /* Merge adjacent */ - u32 write_idx = 0; - for (u32 i = 0; i < free_count; i++) { - if (write_idx > 0 && - blocks[write_idx - 1].offset + blocks[write_idx - 1].size == blocks[i].offset) { - blocks[write_idx - 1].size += blocks[i].size; - } else { - if (write_idx != i) { - blocks[write_idx] = blocks[i]; - } - write_idx++; - } - } +void memfile_free(memfile_t *mf, u64 offset, u64 size) { + if (!offset) return; + u64 n = round_up32(size); +#ifdef MEMFILE_DOUBLE_FREE_CHECK + mf_assert_allocated(mf, offset, n); +#endif - /* Rebuild free list in offset order */ - mf->header->free_list_head = blocks[0].offset; - for (u32 i = 0; i < write_idx; i++) { - memfile_free_t *block = (memfile_free_t*)memfile_ptr(mf, blocks[i].offset); - block->size = blocks[i].size; - block->next = (i + 1 < write_idx) ? blocks[i + 1].offset : 0; - } + /* merge with the immediately-following free block, if physically adjacent */ + u64 s = mf_succ(mf, offset); + if (s && offset + n == s) { n += ND(mf, s)->size; mf_remove(mf, s); } - free(blocks); + /* merge with the immediately-preceding free block, if physically adjacent */ + u64 p = mf_pred(mf, offset); + if (p && p + ND(mf, p)->size == offset) { u64 psz = ND(mf, p)->size; mf_remove(mf, p); offset = p; n += psz; } + + mf_insert(mf, offset, n); } +void memfile_coalesce(memfile_t *mf) { (void)mf; } /* continuous: nothing to do */ + /* ========================================================================= * Refresh mapping after another process grows the file * ========================================================================= */ @@ -244,25 +233,19 @@ void memfile_coalesce(memfile_t *mf) { int memfile_refresh(memfile_t *mf) { struct stat st; if (fstat(mf->fd, &st) < 0) return -1; - - size_t actual_size = (size_t)st.st_size; - if (actual_size <= mf->mmap_size) return 0; /* No growth detected */ - - /* File grew — remap to cover the new size */ + size_t actual = (size_t)st.st_size; + if (actual <= mf->mmap_size) return 0; #ifdef __linux__ - void *new_base = mremap(mf->mmap_base, mf->mmap_size, actual_size, MREMAP_MAYMOVE); - if (new_base == MAP_FAILED) return -1; + void *nb = mremap(mf->mmap_base, mf->mmap_size, actual, MREMAP_MAYMOVE); + if (nb == MAP_FAILED) return -1; #else munmap(mf->mmap_base, mf->mmap_size); - void *new_base = mmap(NULL, actual_size, PROT_READ | PROT_WRITE, - MAP_SHARED, mf->fd, 0); - if (new_base == MAP_FAILED) return -1; + void *nb = mmap(NULL, actual, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); + if (nb == MAP_FAILED) return -1; #endif - - mf->mmap_base = new_base; - mf->mmap_size = actual_size; - mf->header = (memfile_header_t*)new_base; - + mf->mmap_base = nb; + mf->mmap_size = actual; + mf->header = (memfile_header_t *)nb; return 0; } @@ -270,25 +253,18 @@ int memfile_refresh(memfile_t *mf) { * Concurrency - POSIX flock * ========================================================================= */ -int memfile_lock_shared(memfile_t *mf) { - return flock(mf->fd, LOCK_SH); -} - -int memfile_lock_exclusive(memfile_t *mf) { - return flock(mf->fd, LOCK_EX); -} - -int memfile_unlock(memfile_t *mf) { - return flock(mf->fd, LOCK_UN); -} +int memfile_lock_shared(memfile_t *mf) { return flock(mf->fd, LOCK_SH); } +int memfile_lock_exclusive(memfile_t *mf) { return flock(mf->fd, LOCK_EX); } +int memfile_unlock(memfile_t *mf) { return flock(mf->fd, LOCK_UN); } /* ========================================================================= - * Open/close + * Open / close / sync * ========================================================================= */ memfile_t *memfile_open(const char *path, size_t initial_size) { memfile_t *mf = calloc(1, sizeof(memfile_t)); if (!mf) return NULL; + mf->fd = -1; mf->path = strdup(path); struct stat st; @@ -297,44 +273,32 @@ memfile_t *memfile_open(const char *path, size_t initial_size) { if (exists) { mf->fd = open(path, O_RDWR); if (mf->fd < 0) goto fail; - - mf->mmap_size = st.st_size; - mf->mmap_base = mmap(NULL, mf->mmap_size, PROT_READ | PROT_WRITE, - MAP_SHARED, mf->fd, 0); + mf->mmap_size = (size_t)st.st_size; + mf->mmap_base = mmap(NULL, mf->mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); if (mf->mmap_base == MAP_FAILED) goto fail_fd; - - mf->header = (memfile_header_t*)mf->mmap_base; - - if (mf->header->magic != MEMFILE_MAGIC) { + mf->header = (memfile_header_t *)mf->mmap_base; + /* Refuse anything that is not a v3 file: never misread/corrupt older data. */ + if (mf->header->magic != MEMFILE_MAGIC || mf->header->version != MEMFILE_VERSION) { munmap(mf->mmap_base, mf->mmap_size); goto fail_fd; } } else { mf->fd = open(path, O_RDWR | O_CREAT, 0644); if (mf->fd < 0) goto fail; - - if (initial_size < sizeof(memfile_header_t) + 64) { - initial_size = 4096; - } - + if (initial_size < sizeof(memfile_header_t) + 64) initial_size = 4096; if (ftruncate(mf->fd, initial_size) < 0) goto fail_fd; - mf->mmap_size = initial_size; - mf->mmap_base = mmap(NULL, mf->mmap_size, PROT_READ | PROT_WRITE, - MAP_SHARED, mf->fd, 0); - if (mf->mmap_base == MAP_FAILED) { - unlink(path); - goto fail_fd; - } - - mf->header = (memfile_header_t*)mf->mmap_base; + mf->mmap_base = mmap(NULL, mf->mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, mf->fd, 0); + if (mf->mmap_base == MAP_FAILED) { unlink(path); goto fail_fd; } + mf->header = (memfile_header_t *)mf->mmap_base; mf->header->magic = MEMFILE_MAGIC; mf->header->version = MEMFILE_VERSION; mf->header->file_size = initial_size; - mf->header->allocated = sizeof(memfile_header_t); - mf->header->free_list_head = 0; + mf->header->allocated = sizeof(memfile_header_t); /* 64: 32-aligned bump start */ + mf->header->free_root = 0; + mf->header->free_bytes = 0; + mf->header->free_count = 0; } - return mf; fail_fd: @@ -354,7 +318,7 @@ void memfile_close(memfile_t *mf) { if (!mf || mf->closed) return; mf->closed = 1; memfile_sync(mf); - munmap(mf->mmap_base, mf->mmap_size); - close(mf->fd); + if (mf->mmap_base) munmap(mf->mmap_base, mf->mmap_size); + if (mf->fd >= 0) close(mf->fd); free(mf->path); } diff --git a/native/memoryfile.h b/native/memoryfile.h index 098f2e8..ede5d1b 100644 --- a/native/memoryfile.h +++ b/native/memoryfile.h @@ -1,13 +1,16 @@ /* - * Memory File - mmap-based arena allocator with automatic growth - * - * The file header IS the arena struct (packed, at offset 0). - * All allocations return offsets, not pointers. - * Pointers become invalid after mremap, offsets remain valid. + * Memory File v3 - mmap-backed arena with a Cartesian-tree free allocator. * - * Originally from biscuit/server; adapted for MCP memory server. + * The header IS the arena descriptor (packed, at offset 0). All allocations are + * OFFSETS, not pointers (stable across mremap). Live blocks carry ZERO metadata; + * free blocks hold a Cartesian-tree node (BST by address, max-heap by size) in + * their own bytes => O(log n) best-fit + continuous coalescing, zero live overhead. + * + * Deallocation is SIZED: the caller passes the allocation size back to free() + * (sized-deallocation; this is what removes the per-allocation size header). + * + * Format v3 is NOT compatible with v1: memfile_open refuses version != 3. */ - #ifndef MEMORYFILE_H #define MEMORYFILE_H @@ -19,37 +22,28 @@ typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; -#define MEMFILE_MAGIC 0x4D454D46 /* "MEMF" */ -#define MEMFILE_VERSION 1 +#define MEMFILE_MAGIC 0x4D454D46u /* "MEMF" */ +#define MEMFILE_VERSION 3 -/* File header - lives at offset 0, IS the arena */ +/* File header at offset 0. Padded to 64B so `allocated` starts 32-aligned. */ typedef struct __attribute__((packed)) { u32 magic; u32 version; - u64 file_size; /* Current file size */ - u64 allocated; /* Bump pointer: next allocation from end */ - u64 free_list_head; /* Offset to first free block (0=none) */ + u64 file_size; /* current file/mmap size */ + u64 allocated; /* bump cursor: next bump allocation starts here */ + u64 free_root; /* Cartesian-tree root offset (0 = empty) */ + u64 free_bytes; /* total free bytes (sum of free blocks); O(1) maintained */ + u64 free_count; /* number of free blocks; O(1) maintained */ + u64 _pad[2]; } memfile_header_t; -/* Free block header - lives IN the free space it describes */ -typedef struct __attribute__((packed)) { - u64 size; /* Size of this free block (including header) */ - u64 next; /* Offset to next free block (0=none) */ -} memfile_free_t; - -/* Allocation header - immediately before each allocated block */ -typedef struct __attribute__((packed)) { - u64 size; /* Size of allocation (including this header) */ -} memfile_alloc_t; - -/* Handle for working with memory file */ typedef struct { int fd; char *path; - void *mmap_base; /* Base address of mmap */ - size_t mmap_size; /* Current mmap size */ - memfile_header_t *header; /* Points to offset 0 */ - int closed; /* Set after close to prevent double-free */ + void *mmap_base; + size_t mmap_size; + memfile_header_t *header; /* points to offset 0 */ + int closed; } memfile_t; /* Lifecycle */ @@ -57,18 +51,18 @@ memfile_t *memfile_open(const char *path, size_t initial_size); void memfile_close(memfile_t *mf); void memfile_sync(memfile_t *mf); -/* Allocation - returns offset from file start (0 = failed) */ +/* Allocation - returns an offset from file start (0 = failed). Live blocks carry no header. */ u64 memfile_alloc(memfile_t *mf, u64 size); -void memfile_free(memfile_t *mf, u64 offset); - -/* Defragmentation */ +/* SIZED free - pass the same `size` that was requested at alloc time. */ +void memfile_free(memfile_t *mf, u64 offset, u64 size); +/* Coalescing is CONTINUOUS (performed in free); kept as a no-op for ABI stability. */ void memfile_coalesce(memfile_t *mf); /* Direct read/write at offset */ int memfile_read(memfile_t *mf, u64 offset, void *buf, u64 len); int memfile_write(memfile_t *mf, u64 offset, const void *buf, u64 len); -/* Convert offset to pointer (CAUTION: invalid after alloc that triggers remap) */ +/* Convert offset to pointer (CAUTION: invalid after an alloc that triggers remap) */ void *memfile_ptr(memfile_t *mf, u64 offset); /* Refresh mapping if the file was grown by another process */ diff --git a/native/schema.h b/native/schema.h new file mode 100644 index 0000000..3685abd --- /dev/null +++ b/native/schema.h @@ -0,0 +1,27 @@ +// schema.h - Bidirectional lens framework for versioned structs +#ifndef SCHEMA_H +#define SCHEMA_H + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +//============================================================================= +// LENS_COPY: smart copy that handles both scalar and array types +// Uses _Generic to dispatch based on destination type +//============================================================================= + +#define LENS_COPY(dst, src) \ + _Generic((dst), \ + char*: memcpy((dst), (src), sizeof(dst)), \ + default: (dst) = (src) \ + ) + +#endif // SCHEMA_H diff --git a/native/stringtable.c b/native/stringtable.c new file mode 100644 index 0000000..07b9496 --- /dev/null +++ b/native/stringtable.c @@ -0,0 +1,222 @@ +#include "stringtable.h" + +#include +#include + +#define ENT_HEADER 10u /* u32 refcount + u32 hash + u16 len */ +#define OUR_HEADER_SIZE 16u /* u64 hash_index_offset + u32 entry_count + u32 pad */ +#define INITIAL_BUCKETS 4096u + +/* ---- aliasing-safe field access via memfile offsets (never cache across alloc) ---- */ +static inline u32 rdu32(memfile_t *mf, u64 o) { u32 v; memcpy(&v, memfile_ptr(mf, o), 4); return v; } +static inline u16 rdu16(memfile_t *mf, u64 o) { u16 v; memcpy(&v, memfile_ptr(mf, o), 2); return v; } +static inline u64 rdu64(memfile_t *mf, u64 o) { u64 v; memcpy(&v, memfile_ptr(mf, o), 8); return v; } +static inline void wru32(memfile_t *mf, u64 o, u32 v) { memcpy(memfile_ptr(mf, o), &v, 4); } +static inline void wru16(memfile_t *mf, u64 o, u16 v) { memcpy(memfile_ptr(mf, o), &v, 2); } +static inline void wru64(memfile_t *mf, u64 o, u64 v) { memcpy(memfile_ptr(mf, o), &v, 8); } + +static u32 fnv1a(const u8 *d, u16 len) { + u32 h = 0x811c9dc5u; + for (u16 i = 0; i < len; i++) { h ^= d[i]; h *= 0x01000193u; } + return h; +} + +/* ---- header / index accessors ---- */ +static inline u64 hash_index_off(stringtable_t *st) { return rdu64(st->mf, st->header_offset + 0); } +static inline u32 entry_count(stringtable_t *st) { return rdu32(st->mf, st->header_offset + 8); } +static inline void set_entry_count(stringtable_t *st, u32 c) { wru32(st->mf, st->header_offset + 8, c); } +static inline u64 bucket_pos(u64 idx, u32 slot) { return idx + 8 + (u64)slot * 8; } + +static void st_rehash(stringtable_t *st, u32 new_bc); + +/* ---- init ---- */ +static u64 st_init(stringtable_t *st) { + memfile_t *mf = st->mf; + u64 hdr = memfile_alloc(mf, OUR_HEADER_SIZE); + u64 idx_size = 8 + (u64)INITIAL_BUCKETS * 8; + u64 idx = memfile_alloc(mf, idx_size); + if (!hdr || !idx) return 0; + memset(memfile_ptr(mf, idx), 0, idx_size); + wru32(mf, idx + 0, INITIAL_BUCKETS); /* bucket_count */ + wru64(mf, hdr + 0, idx); /* hash_index_offset */ + wru32(mf, hdr + 8, 0); /* entry_count */ + return hdr; +} + +stringtable_t *st_open(const char *path, size_t initial_size) { + stringtable_t *st = calloc(1, sizeof(*st)); + if (!st) return NULL; + st->mf = memfile_open(path, initial_size ? initial_size : 65536); + if (!st->mf) { free(st); return NULL; } + + st_lock_exclusive(st); + memfile_refresh(st->mf); + if (st->mf->header->allocated <= sizeof(memfile_header_t)) { + st->header_offset = st_init(st); + memfile_sync(st->mf); + } else { + st->header_offset = sizeof(memfile_header_t); /* the file's first allocation */ + } + st_unlock(st); + + if (st->header_offset == 0) { st_close(st); return NULL; } + return st; +} + +/* ---- intern / find ---- */ +u64 st_intern(stringtable_t *st, const u8 *data, u16 len) { + memfile_t *mf = st->mf; + u32 hash = fnv1a(data, len); + u64 idx = hash_index_off(st); + u32 bc = rdu32(mf, idx + 0); + u32 bucket = hash % bc; + + for (u32 i = 0; i < bc; i++) { + u32 slot = (bucket + i) % bc; + u64 eoff = rdu64(mf, bucket_pos(idx, slot)); + + if (eoff == 0) { /* empty -> new entry */ + u64 noff = memfile_alloc(mf, ENT_HEADER + len); + if (!noff) return 0; + wru32(mf, noff + 0, 1); /* refcount */ + wru32(mf, noff + 4, hash); + wru16(mf, noff + 8, len); + if (len) memcpy(memfile_ptr(mf, noff + 10), data, len); + + idx = hash_index_off(st); /* re-fetch (offset stable pre-rehash) */ + wru64(mf, bucket_pos(idx, slot), noff); + u32 cnt = entry_count(st) + 1; + set_entry_count(st, cnt); + if ((u64)cnt * 10 > (u64)bc * 7) st_rehash(st, bc * 2); + return noff; + } + + if (rdu32(mf, eoff + 4) == hash) { /* hash hit -> compare */ + u16 elen = rdu16(mf, eoff + 8); + if (elen == len && (len == 0 || memcmp(memfile_ptr(mf, eoff + 10), data, len) == 0)) { + wru32(mf, eoff + 0, rdu32(mf, eoff + 0) + 1); /* refcount++ */ + return eoff; + } + } + } + return 0; /* index full — should not happen with rehashing */ +} + +u64 st_find(stringtable_t *st, const u8 *data, u16 len) { + memfile_t *mf = st->mf; + u32 hash = fnv1a(data, len); + u64 idx = hash_index_off(st); + u32 bc = rdu32(mf, idx + 0); + u32 bucket = hash % bc; + + for (u32 i = 0; i < bc; i++) { + u32 slot = (bucket + i) % bc; + u64 eoff = rdu64(mf, bucket_pos(idx, slot)); + if (eoff == 0) return 0; + if (rdu32(mf, eoff + 4) == hash) { + u16 elen = rdu16(mf, eoff + 8); + if (elen == len && (len == 0 || memcmp(memfile_ptr(mf, eoff + 10), data, len) == 0)) + return eoff; + } + } + return 0; +} + +/* ---- refcount ---- */ +void st_addref(stringtable_t *st, u64 id) { + if (!id) return; + wru32(st->mf, id + 0, rdu32(st->mf, id + 0) + 1); +} + +/* circular-probe relocation test (Knuth backward-shift deletion) */ +static int needs_reloc(u32 natural, u32 empty, u32 current) { + if (natural <= current) return natural <= empty && empty < current; + return natural <= empty || empty < current; +} +static void index_fixup(stringtable_t *st, u32 removed, u32 bc) { + memfile_t *mf = st->mf; + u64 idx = hash_index_off(st); + u32 slot = (removed + 1) % bc; + for (;;) { + u64 e = rdu64(mf, bucket_pos(idx, slot)); + if (e == 0) break; + u32 natural = rdu32(mf, e + 4) % bc; + if (needs_reloc(natural, removed, slot)) { + wru64(mf, bucket_pos(idx, removed), e); + wru64(mf, bucket_pos(idx, slot), 0); + removed = slot; + } + slot = (slot + 1) % bc; + } +} +static void index_remove(stringtable_t *st, u64 off, u32 hash) { + memfile_t *mf = st->mf; + u64 idx = hash_index_off(st); + u32 bc = rdu32(mf, idx + 0); + u32 bucket = hash % bc; + for (u32 i = 0; i < bc; i++) { + u32 slot = (bucket + i) % bc; + u64 e = rdu64(mf, bucket_pos(idx, slot)); + if (e == 0) return; + if (e == off) { wru64(mf, bucket_pos(idx, slot), 0); index_fixup(st, slot, bc); return; } + } +} + +void st_release(stringtable_t *st, u64 id) { + if (!id) return; + memfile_t *mf = st->mf; + u32 rc = rdu32(mf, id + 0); + if (rc <= 1) { + u32 hash = rdu32(mf, id + 4); + u16 len = rdu16(mf, id + 8); + index_remove(st, id, hash); + memfile_free(mf, id, ENT_HEADER + len); /* sized free */ + set_entry_count(st, entry_count(st) - 1); + } else { + wru32(mf, id + 0, rc - 1); + } +} + +/* ---- rehash ---- */ +static void st_rehash(stringtable_t *st, u32 new_bc) { + memfile_t *mf = st->mf; + u64 old_idx = hash_index_off(st); + u32 old_bc = rdu32(mf, old_idx + 0); + u64 new_size = 8 + (u64)new_bc * 8; + u64 new_idx = memfile_alloc(mf, new_size); + if (!new_idx) return; /* leave old index in place; still correct */ + memset(memfile_ptr(mf, new_idx), 0, new_size); + wru32(mf, new_idx + 0, new_bc); + + for (u32 i = 0; i < old_bc; i++) { + u64 e = rdu64(mf, bucket_pos(old_idx, i)); + if (e == 0) continue; + u32 b = rdu32(mf, e + 4) % new_bc; + for (u32 j = 0; j < new_bc; j++) { + u32 s = (b + j) % new_bc; + if (rdu64(mf, bucket_pos(new_idx, s)) == 0) { wru64(mf, bucket_pos(new_idx, s), e); break; } + } + } + wru64(mf, st->header_offset + 0, new_idx); /* repoint header */ + memfile_free(mf, old_idx, 8 + (u64)old_bc * 8); +} + +/* ---- read / stats ---- */ +const u8 *st_get(stringtable_t *st, u64 id, u16 *len_out) { + if (len_out) *len_out = rdu16(st->mf, id + 8); + return (const u8 *)memfile_ptr(st->mf, id + 10); +} +u32 st_refcount(stringtable_t *st, u64 id) { return rdu32(st->mf, id + 0); } +u32 st_count(stringtable_t *st) { return entry_count(st); } + +/* ---- lifecycle / concurrency ---- */ +void st_sync(stringtable_t *st) { memfile_sync(st->mf); } +int st_lock_shared(stringtable_t *st) { return memfile_lock_shared(st->mf); } +int st_lock_exclusive(stringtable_t *st) { return memfile_lock_exclusive(st->mf); } +int st_unlock(stringtable_t *st) { return memfile_unlock(st->mf); } + +void st_close(stringtable_t *st) { + if (!st) return; + if (st->mf) { memfile_close(st->mf); free(st->mf); } + free(st); +} diff --git a/native/stringtable.h b/native/stringtable.h new file mode 100644 index 0000000..c96fa44 --- /dev/null +++ b/native/stringtable.h @@ -0,0 +1,43 @@ +/* + * StringTable - interned, refcounted string storage over a v3 MemoryFile. + * + * Entry layout (allocated via memfile_alloc): [u32 refcount][u32 hash][u16 len][u8 data[len]] + * String ID = the entry offset (v3 has no per-alloc header, so id == alloc offset directly). + * Hash index: [u32 bucket_count][u32 _pad][u64 buckets[bucket_count]], linear probing. + * Our header block (first allocation): [u64 hash_index_offset][u32 entry_count][u32 _pad]. + */ +#ifndef STRINGTABLE_H +#define STRINGTABLE_H + +#include "memoryfile.h" + +typedef struct { + memfile_t *mf; + u64 header_offset; /* our header block (the file's first allocation) */ +} stringtable_t; + +stringtable_t *st_open(const char *path, size_t initial_size); +void st_close(stringtable_t *st); +void st_sync(stringtable_t *st); + +/* Intern: dedup + refcount++. Returns id (offset); 0 on failure. */ +u64 st_intern(stringtable_t *st, const u8 *data, u16 len); +/* Look up without interning / bumping. Returns id, or 0 if absent. */ +u64 st_find(stringtable_t *st, const u8 *data, u16 len); +/* refcount++ on an existing id. */ +void st_addref(stringtable_t *st, u64 id); +/* refcount--; frees the entry + removes from index when it hits 0. */ +void st_release(stringtable_t *st, u64 id); + +/* Zero-copy read: returns a pointer into the mmap + length. Valid until the next + * allocation/remap on this table. */ +const u8 *st_get(stringtable_t *st, u64 id, u16 *len_out); +u32 st_refcount(stringtable_t *st, u64 id); +u32 st_count(stringtable_t *st); + +/* Concurrency passthrough (strings file has its own fd/flock). */ +int st_lock_shared(stringtable_t *st); +int st_lock_exclusive(stringtable_t *st); +int st_unlock(stringtable_t *st); + +#endif /* STRINGTABLE_H */ diff --git a/native/test_doublefree.c b/native/test_doublefree.c new file mode 100644 index 0000000..03139d6 --- /dev/null +++ b/native/test_doublefree.c @@ -0,0 +1,20 @@ +/* + * Negative test for the test-only double-free detector. A deliberate double-free + * MUST trip MEMFILE_DOUBLE_FREE_CHECK and abort. Driven by `make verify-detector`, + * which asserts the detector message is printed. Reaching `return 0` = the + * detector FAILED to fire. + */ +#include "memoryfile.h" +#include + +int main(void) { + const char *path = "/tmp/mf_doublefree_scratch.dat"; + unlink(path); + memfile_t *mf = memfile_open(path, 65536); + if (!mf) return 2; + u64 a = memfile_alloc(mf, 100); + memfile_free(mf, a, 100); + memfile_free(mf, a, 100); /* detector must abort here */ + unlink(path); + return 0; +} diff --git a/native/test_entity.c b/native/test_entity.c new file mode 100644 index 0000000..86df9df --- /dev/null +++ b/native/test_entity.c @@ -0,0 +1,33 @@ +/* Validate the biscuit-derived versioned record machinery on our Entity schema. */ +#include +#include +#include "entity.h" + +int main(void) { + int ok = 1; + printf("sizeof(Entity)=%zu (expect 72)\n", sizeof(Entity)); + printf("entity_bufsize(1)=%zu (expect 76)\n", entity_bufsize(1)); + if (sizeof(Entity) != 72) ok = 0; + if (entity_bufsize(1) != 76) ok = 0; + + Entity e; memset(&e, 0, sizeof(e)); + e.name_id = 42; e.type_id = 7; e.adj_offset = 0x1234; e.mtime = 999; + e.obs_count = 2; e.obs0_id = 11; e.obs1_id = 12; + e.structural_visits = 5; e.walker_visits = 9; e.psi = 3.14159; + + u8 buf[128]; + entity_write(&e, buf); + u32 stored_ver = *(u32 *)buf; + + Entity r; + int rc = entity_read(buf, &r); + printf("read rc=%d stored_ver=%u name_id=%u psi=%g walker=%llu obs1=%u\n", + rc, stored_ver, r.name_id, r.psi, (unsigned long long)r.walker_visits, r.obs1_id); + if (rc != 0 || stored_ver != 1) ok = 0; + if (r.name_id != 42 || r.type_id != 7 || r.adj_offset != 0x1234 || r.mtime != 999) ok = 0; + if (r.obs_count != 2 || r.obs0_id != 11 || r.obs1_id != 12) ok = 0; + if (r.structural_visits != 5 || r.walker_visits != 9 || r.psi != 3.14159) ok = 0; + + printf(ok ? "\nALL PASS\n" : "\nFAILED\n"); + return ok ? 0 : 1; +} diff --git a/native/test_graph.c b/native/test_graph.c new file mode 100644 index 0000000..74f4ea7 --- /dev/null +++ b/native/test_graph.c @@ -0,0 +1,246 @@ +/* + * Unified graph store harness: fuzz create/delete entity+relation + observations, + * validate name-index/lookup + scan ops every so often, then tear down and assert + * the string table empties (any name/type/relType/observation leak shows up here). + * Run under ASan+UBSan. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include "stringtable.h" +#include "graph.h" + +static int fails = 0; +#define CHECK(c, m) do { if (!(c)) { printf(" FAIL: %s\n", m); fails++; } else printf(" ok: %s\n", m); } while (0) + +static u64 rs = 0xcafef00d1234ull; +static u64 xs(void) { u64 x = rs; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return rs = x; } + +#define NENT 400 +#define NRT 8 + +typedef struct { char name[16]; char type[16]; u64 off; int alive; } Ent; +typedef struct { int from, to, rt; } Rel; + +static Ent ents[NENT]; +static u32 obsn[NENT]; /* observation count per live entity (0..2) */ +static Rel *rels; static size_t nrel, relcap; + +static void rel_push(int f, int t, int r) { + if (nrel == relcap) { relcap = relcap ? relcap * 2 : 1024; rels = realloc(rels, relcap * sizeof(Rel)); } + rels[nrel++] = (Rel){ f, t, r }; +} +static int rel_find(int f, int t, int r) { + for (size_t i = 0; i < nrel; i++) if (rels[i].from == f && rels[i].to == t && rels[i].rt == r) return (int)i; + return -1; +} +static void rel_del_idx(size_t i) { rels[i] = rels[--nrel]; } +static void rel_drop_incident(int e) { + for (size_t i = 0; i < nrel; ) { if (rels[i].from == e || rels[i].to == e) rel_del_idx(i); else i++; } +} +static void rtname(int r, char *buf) { snprintf(buf, 16, "rel-%d", r); } + +static graph_t *gr; static stringtable_t *st; + +static int cmp_u64t(const void *a, const void *b) { u64 x = *(const u64 *)a, y = *(const u64 *)b; return (x > y) - (x < y); } + +static int count_alive(void) { int c = 0; for (int i = 0; i < NENT; i++) if (ents[i].alive) c++; return c; } + +static int validate(void) { + int bad = 0; + if ((int)graph_entity_count(gr) != count_alive()) bad++; + for (int i = 0; i < NENT; i++) { + u64 found = graph_lookup(gr, (const u8 *)ents[i].name, (u16)strlen(ents[i].name)); + if (ents[i].alive) { if (found != ents[i].off) bad++; } + else { if (found != 0) bad++; } + } + return bad; +} + +static int pick_alive(void) { + int n = count_alive(); if (!n) return -1; + int k = (int)(xs() % (u64)n); + for (int i = 0; i < NENT; i++) if (ents[i].alive && k-- == 0) return i; + return -1; +} + +int main(void) { + const char *gp = "/tmp/graph_test.dat", *sp = "/tmp/graph_test.strings.dat"; + unlink(gp); unlink(sp); + st = st_open(sp, 1u << 16); + gr = graph_open(gp, st, 1u << 16); + if (!st || !gr) { printf("open failed\n"); return 2; } + + for (int i = 0; i < NENT; i++) { + snprintf(ents[i].name, 16, "ent-%d", i); + snprintf(ents[i].type, 16, "type-%d", i % 16); + ents[i].off = 0; ents[i].alive = 0; obsn[i] = 0; + } + + size_t bad = 0; + char rb[16]; + for (size_t it = 0; it < 200000; it++) { + u32 op = (u32)(xs() % 100); + if (op < 42) { /* create entity */ + int i = (int)(xs() % NENT); + u64 off = graph_create_entity(gr, (const u8 *)ents[i].name, (u16)strlen(ents[i].name), + (const u8 *)ents[i].type, (u16)strlen(ents[i].type), it); + if (ents[i].alive) { if (off != ents[i].off) bad++; } + else { ents[i].alive = 1; ents[i].off = off; obsn[i] = 0; } + } else if (op < 56) { /* delete entity */ + int i = pick_alive(); if (i < 0) continue; + rel_drop_incident(i); + graph_delete_entity(gr, ents[i].off); + ents[i].alive = 0; obsn[i] = 0; + } else if (op < 64) { /* add observation */ + int i = pick_alive(); if (i < 0) continue; + if (obsn[i] < 2) { + char ob[24]; int l = snprintf(ob, sizeof ob, "o-%d-%u", i, obsn[i]); + graph_add_observation(gr, ents[i].off, (const u8 *)ob, (u16)l, it); + obsn[i]++; + } + } else if (op < 88) { /* create relation */ + int a = pick_alive(), b = pick_alive(); + if (a < 0 || b < 0 || a == b) continue; + int r = (int)(xs() % NRT); + if (rel_find(a, b, r) < 0) { + rtname(r, rb); + graph_create_relation(gr, ents[a].off, ents[b].off, (const u8 *)rb, (u16)strlen(rb), it); + rel_push(a, b, r); + } + } else { /* delete relation */ + if (nrel == 0) continue; + size_t k = xs() % nrel; + Rel rr = rels[k]; rtname(rr.rt, rb); + graph_delete_relation(gr, ents[rr.from].off, ents[rr.to].off, (const u8 *)rb, (u16)strlen(rb)); + rel_del_idx(k); + } + if ((it & 0xFFF) == 0 && validate() != 0) bad++; + } + CHECK(bad == 0, "per-op model: entity count + name-index lookups consistent"); + + /* scan-op spot checks against the model */ + CHECK((int)graph_entity_count(gr) == count_alive(), "entity_count == model"); + CHECK(graph_relation_count(gr) == nrel, "relation_count == model relation set size"); + { + int model = 0; for (int i = 0; i < NENT; i++) if (ents[i].alive && (i % 16) == 3) model++; + u64 *buf = malloc((size_t)NENT * sizeof(u64)); + u32 got = graph_entities_by_type(gr, (const u8 *)"type-3", 6, buf, NENT); + CHECK((int)got == model, "entities_by_type == model"); + free(buf); + } + { + u32 *tb = malloc(64 * sizeof(u32)); + u32 net = graph_entity_types(gr, tb, 64); + u32 nrt2 = graph_relation_types(gr, tb, 64); + printf(" distinct entity_types=%u relation_types=%u orphaned=%u\n", + net, nrt2, graph_orphaned(gr, (u64 *)tb, 0)); + free(tb); + } + printf(" mid-run: live=%d relations=%zu strings=%u\n", count_alive(), nrel, st_count(st)); + + /* traversal: get_neighbors (fwd depth-1) vs model, find_path on a direct edge */ + { + int nb_ok = 1, samples = 0; + for (int i = 0; i < NENT && samples < 40; i++) { + if (!ents[i].alive) continue; + samples++; + u64 mset[NENT]; u32 mn = 0; + for (size_t k = 0; k < nrel; k++) if (rels[k].from == i) { + u64 o = ents[rels[k].to].off; int dup = 0; + for (u32 j = 0; j < mn; j++) if (mset[j] == o) { dup = 1; break; } + if (!dup) mset[mn++] = o; + } + u64 gset[NENT]; u32 gn = graph_neighbors(gr, ents[i].off, 1, DIR_FORWARD, gset, NENT); + if (gn != mn) { nb_ok = 0; continue; } + qsort(mset, mn, 8, cmp_u64t); qsort(gset, gn, 8, cmp_u64t); + for (u32 j = 0; j < mn; j++) if (mset[j] != gset[j]) { nb_ok = 0; break; } + } + CHECK(nb_ok, "get_neighbors (fwd depth-1) matches model over 40 samples"); + + int fp_ok = 1; + if (nrel > 0) { + Rel rr = rels[0]; + u64 path[64]; + u32 pl = graph_find_path(gr, ents[rr.from].off, ents[rr.to].off, 4, DIR_FORWARD, path, 64); + if (!(pl == 2 && path[0] == ents[rr.from].off && path[1] == ents[rr.to].off)) fp_ok = 0; + if (graph_find_path(gr, ents[rr.from].off, ents[rr.from].off, 4, DIR_ANY, path, 64) != 1) fp_ok = 0; + } + CHECK(fp_ok, "find_path: direct edge -> len 2, self -> len 1"); + } + + /* search: POSIX ERE over name/type/obs, full result set */ + { + u64 *sb = malloc((size_t)NENT * sizeof(u64)); + u32 s1 = graph_search(gr, "^ent-7$", sb, NENT); + CHECK((int)s1 == (ents[7].alive ? 1 : 0), "search ^ent-7$ matches the name exactly"); + int model = 0; for (int i = 0; i < NENT; i++) if (ents[i].alive && (i % 16) == 3) model++; + u32 s2 = graph_search(gr, "type-3", sb, NENT); + CHECK((int)s2 == model, "search type-3 (type field) == model"); + free(sb); + } + + /* ranking: structural sampling, MERW psi, random walk, walker counting */ + { + graph_seed_rng(12345); + u32 sv = graph_structural_sample(gr, 1, 0.85); + CHECK(sv > 0 && graph_structural_total(gr) == sv, "structural sample: visits recorded == structural_total"); + + u32 it = graph_compute_merw_psi(gr, 0.85, 200, 1e-8); + CHECK(it > 0 && it <= 200, "MERW psi power-iteration converged"); + double psisum = 0; + for (int i = 0; i < NENT; i++) if (ents[i].alive) psisum += graph_get_psi(gr, ents[i].off); + CHECK(psisum > 0, "MERW psi populated across live entities"); + + u64 wt0 = graph_walker_total(gr); + int wi = pick_alive(); + if (wi >= 0) { + graph_inc_walker_visit(gr, ents[wi].off); + CHECK(graph_walker_total(gr) == wt0 + 1, "walker visit bumps walker_total"); + CHECK(graph_walker_rank(gr, ents[wi].off) > 0.0, "walker rank > 0 after a visit"); + } + + if (nrel > 0) { + u64 path[16]; + u32 pl = graph_random_walk(gr, ents[rels[0].from].off, 5, DIR_FORWARD, 1, 999, path, 16); + CHECK(pl >= 1 && pl <= 6 && path[0] == ents[rels[0].from].off, "random_walk: valid path (start + <=depth steps)"); + } + } + + /* validate_graph: obs limits + dangling edges */ + { + u64 *vo = malloc((size_t)NENT * 8); u8 *vc = malloc(NENT), *vov = malloc(NENT); + u64 *ds = malloc((size_t)NENT * 8), *dt = malloc((size_t)NENT * 8); + CHECK(graph_validate_obs(gr, vo, vc, vov, NENT) == 0, "validate: no obs violations in fuzz data"); + CHECK(graph_validate_dangling(gr, ds, dt, NENT) == 0, "validate: no dangling edges (integrity holds)"); + u64 t = graph_create_entity(gr, (const u8 *)"validate-test", 13, (const u8 *)"vtype", 5, 999); + char big[160]; memset(big, 'x', 150); big[150] = 0; + graph_add_observation(gr, t, (const u8 *)big, 150, 999); + CHECK(graph_validate_obs(gr, vo, vc, vov, NENT) == 1, "validate: flags the >140-byte observation"); + graph_delete_entity(gr, t); + CHECK(graph_validate_obs(gr, vo, vc, vov, NENT) == 0, "validate: clean after removing it"); + free(vo); free(vc); free(vov); free(ds); free(dt); + } + + /* teardown: delete all relations, then all entities -> string table must empty */ + while (nrel > 0) { + Rel rr = rels[nrel - 1]; rtname(rr.rt, rb); + graph_delete_relation(gr, ents[rr.from].off, ents[rr.to].off, (const u8 *)rb, (u16)strlen(rb)); + nrel--; + } + for (int i = 0; i < NENT; i++) if (ents[i].alive) { graph_delete_entity(gr, ents[i].off); ents[i].alive = 0; } + + CHECK(graph_entity_count(gr) == 0, "all entities deleted"); + CHECK(st_count(st) == 0, "string table empty after teardown (no name/type/relType/observation leak)"); + printf(" final strings=%u entity_count=%u\n", st_count(st), graph_entity_count(gr)); + + graph_close(gr); + st_close(st); + free(rels); + printf(fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", fails); + return fails ? 1 : 0; +} diff --git a/native/test_memfile.c b/native/test_memfile.c new file mode 100644 index 0000000..06153f4 --- /dev/null +++ b/native/test_memfile.c @@ -0,0 +1,71 @@ +/* + * v3 memoryfile.c validation: fuzz with overlap detection + version-refuse guard. + * Standalone (no N-API). Run under ASan+UBSan. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include "memoryfile.h" + +static u64 g = 0x123456789abcdefull; +static u64 xs(void) { u64 x = g; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return g = x; } +static u64 r32(u64 s) { s = (s + 31) & ~(u64)31; return s < 32 ? 32 : s; } + +static int fails = 0; +#define CHECK(c, m) do { if (!(c)) { printf(" FAIL: %s\n", m); fails++; } else printf(" ok: %s\n", m); } while (0) + +int main(void) { + const char *p = "/tmp/memfile_v3_test.dat"; + unlink(p); + memfile_t *mf = memfile_open(p, 1u << 16); + if (!mf) { printf("open failed\n"); return 2; } + + typedef struct { u64 o, s, end; } L; + size_t cap = 8192; + L *live = malloc(cap * sizeof(L)); + size_t n = 0, overlaps = 0; + + for (size_t i = 0; i < 300000; i++) { + int a = (n == 0) || (n < cap && (xs() & 1)); + if (a) { + u64 s = 32 + 32 * (xs() % 16); + u64 o = memfile_alloc(mf, s); + if (!o) continue; + u64 end = o + r32(s); + for (size_t k = 0; k < n; k++) if (o < live[k].end && live[k].o < end) { overlaps++; break; } + live[n].o = o; live[n].s = s; live[n].end = end; n++; + } else { + size_t k = xs() % n; + L x = live[k]; live[k] = live[--n]; + memfile_free(mf, x.o, x.s); + } + } + CHECK(overlaps == 0, "no overlapping live allocations across 300k ops"); + printf(" fuzz: live=%zu free_count=%llu free_bytes=%llu allocated=%llu file=%llu\n", + n, (unsigned long long)mf->header->free_count, (unsigned long long)mf->header->free_bytes, + (unsigned long long)mf->header->allocated, (unsigned long long)mf->header->file_size); + + for (size_t k = 0; k < n; k++) memfile_free(mf, live[k].o, live[k].s); + printf(" after free-all: free_count=%llu free_bytes=%llu\n", + (unsigned long long)mf->header->free_count, (unsigned long long)mf->header->free_bytes); + CHECK(mf->header->free_count <= 2, "free-all collapses to <=2 blocks"); + memfile_close(mf); + free(mf); + free(live); + + /* version-refuse guard: a non-v3 file must be rejected, never opened. */ + const char *p2 = "/tmp/memfile_v3_badver.dat"; + unlink(p2); + memfile_t *m2 = memfile_open(p2, 1u << 16); + CHECK(m2 != NULL, "create fresh v3 file"); + if (m2) { m2->header->version = 1; memfile_sync(m2); memfile_close(m2); free(m2); } + memfile_t *m3 = memfile_open(p2, 1u << 16); + CHECK(m3 == NULL, "open refuses version != 3 (protects live v1 KB)"); + if (m3) memfile_close(m3); + + printf(fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", fails); + return fails ? 1 : 0; +} diff --git a/native/test_stringtable.c b/native/test_stringtable.c new file mode 100644 index 0000000..e05a4c0 --- /dev/null +++ b/native/test_stringtable.c @@ -0,0 +1,92 @@ +/* + * StringTable validation: rehash stress + dedup/refcount + model-based fuzz. + * Standalone, run under ASan+UBSan. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include "stringtable.h" + +static int fails = 0; +#define CHECK(c, m) do { if (!(c)) { printf(" FAIL: %s\n", m); fails++; } else printf(" ok: %s\n", m); } while (0) + +static u64 g = 0xabcdef0123ull; +static u64 xs(void) { u64 x = g; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return g = x; } + +int main(void) { + const char *p = "/tmp/st_test.dat"; + unlink(p); + stringtable_t *st = st_open(p, 1u << 16); + if (!st) { printf("st_open failed\n"); return 2; } + + char buf[32]; + + /* T1: rehash stress — 10k distinct interns, all findable with stable ids */ + enum { N = 10000 }; + static u64 ids[N]; + for (int i = 0; i < N; i++) { + int l = snprintf(buf, sizeof buf, "string-%d", i); + ids[i] = st_intern(st, (const u8 *)buf, (u16)l); + if (!ids[i]) { fails++; break; } + } + CHECK(st_count(st) == N, "count == N after N distinct interns (forces rehash)"); + int allfind = 1; + for (int i = 0; i < N; i++) { + int l = snprintf(buf, sizeof buf, "string-%d", i); + if (st_find(st, (const u8 *)buf, (u16)l) != ids[i]) { allfind = 0; break; } + } + CHECK(allfind, "all N findable with stable ids after rehash"); + { + int l = snprintf(buf, sizeof buf, "string-%d", 1234); + u16 gl; const u8 *gd = st_get(st, ids[1234], &gl); + CHECK(gl == (u16)l && memcmp(gd, buf, l) == 0, "st_get returns the correct bytes"); + } + + /* T2: dedup + refcount lifecycle */ + { + const char *s = "duplicate-string"; + u16 sl = (u16)strlen(s); + u64 a = st_intern(st, (const u8 *)s, sl), b = st_intern(st, (const u8 *)s, sl); + CHECK(a == b, "duplicate interns return the same id"); + CHECK(st_refcount(st, a) == 2, "refcount == 2 after two interns"); + st_release(st, a); + CHECK(st_refcount(st, a) == 1, "refcount == 1 after one release"); + st_release(st, a); + CHECK(st_find(st, (const u8 *)s, sl) == 0, "entry freed + unindexed when refcount hits 0"); + } + + /* T3: model-based fuzz — parallel-track expected refcounts, validate every step */ + enum { P = 2000 }; + static u32 rc[P]; static u64 pid[P]; + for (int i = 0; i < P; i++) { rc[i] = 0; pid[i] = 0; } + size_t bad = 0; + for (size_t it = 0; it < 400000; it++) { + int i = (int)(xs() % P); + int l = snprintf(buf, sizeof buf, "pool-%d", i); + if (rc[i] == 0 || (xs() & 1)) { /* intern */ + u64 id = st_intern(st, (const u8 *)buf, (u16)l); + if (rc[i] > 0 && id != pid[i]) bad++; /* dedup must return existing id */ + pid[i] = id; rc[i]++; + } else { /* release */ + st_release(st, pid[i]); rc[i]--; + } + if (rc[i] > 0) { + if (st_refcount(st, pid[i]) != rc[i]) bad++; + if (st_find(st, (const u8 *)buf, (u16)l) != pid[i]) bad++; + } else { + if (st_find(st, (const u8 *)buf, (u16)l) != 0) bad++; + } + } + CHECK(bad == 0, "model-based fuzz: refcounts + finds stay consistent"); + for (int i = 0; i < P; i++) while (rc[i] > 0) { st_release(st, pid[i]); rc[i]--; } + CHECK(st_count(st) == N, "after releasing the whole pool, only the T1 strings remain"); + printf(" fuzz bad=%zu final_count=%u (expected %d)\n", bad, st_count(st), N); + + st_close(st); + printf(fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", fails); + return fails ? 1 : 0; +} diff --git a/server.ts b/server.ts index d39d454..389a4dd 100644 --- a/server.ts +++ b/server.ts @@ -11,12 +11,8 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; -import { GraphFile, DIR_FORWARD, DIR_BACKWARD, type EntityRecord, type AdjEntry } from './src/graphfile.js'; -import { StringTable } from './src/stringtable.js'; -import { TrigramIndex } from './src/trigram.js'; -import { buildTrigramQuery } from './src/regex_query.js'; -import { structuralSample } from './src/pagerank.js'; -import { computeMerwPsi } from './src/merw.js'; +import { Store, DIR_FORWARD, DIR_BACKWARD, type NativeEntity } from './src/store.js'; +import { ensureV3 } from './src/migrate.js'; import { validateExtension, loadDocument, type KbLoadResult } from './src/kb_load.js'; import { toolDurationHistogram, traced, tracer } from './src/tracing.js'; @@ -356,30 +352,7 @@ function paginateGraph(graph: KnowledgeGraph, entityCursor: number = 0, relation // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph export class KnowledgeGraphManager { - private gf: GraphFile; - private st: StringTable; - /** In-memory name→offset index, rebuilt from node log on every lock acquisition */ - private nameIndex: Map; - - /** - * Trigram inverted index for fast `search_nodes` regex pre-filtering. Lazy: - * stays `null` until the first `searchNodes` call that supplies non-empty - * `literals` — sessions that never use literals never pay the build cost. - * - * Lifecycle: - * - Built lazily under `withReadLock` / `withWriteLock` on demand. - * - Maintained incrementally by every mutation method *in this process* - * (createEntities, deleteEntities, addObservations, deleteObservations). - * - Dropped to `null` whenever a cross-process change is detected on lock - * acquisition. Next `searchNodes` with literals rebuilds. - * - * v1 limitation: cross-process invalidation is "drop and rebuild" — - * potentially several seconds on a 40K-entity KB. Single-process workloads - * (the common case) only pay the build cost once per process startup. - */ - private trigramIndex: TrigramIndex | null; - /** Snapshot of (entityCount, stringsAllocated) at the time the trigram index was last (re)built. */ - private trigramFingerprint: { entityCount: number; stringsAllocated: bigint }; + private db: Store; constructor(memoryFilePath: string = DEFAULT_MEMORY_FILE_PATH) { // Derive binary file paths from the base path @@ -388,25 +361,23 @@ export class KnowledgeGraphManager { const graphPath = path.join(dir, `${base}.graph`); const strPath = path.join(dir, `${base}.strings`); - // Subobject constructors self-lock around their own init/migration paths - // (see StringTable / GraphFile constructors). They may grow the underlying - // files, so we cannot hold an outer lock across these calls. - this.st = new StringTable(strPath); - this.gf = new GraphFile(graphPath, this.st); - this.nameIndex = new Map(); - this.trigramIndex = null; - this.trigramFingerprint = { entityCount: -1, stringsAllocated: 0n }; - - // Build the in-memory name index and run initial structural sampling + - // MERW under an exclusive lock that covers BOTH the graph and strings - // files. withWriteLock's prelude calls rebuildNameIndex() unconditionally, - // so the name map is populated by the time the callback runs. - // structuralSample + computeMerwPsi mutate the graph file; withWriteLock - // syncs both files on exit. + // Auto-migrate an old (v1/v2) KB to v3 in place before opening — the same + // transparent-on-open contract the old code used for v1->v2. ensureV3 holds + // a migration flock across detection AND migration, so concurrent startups + // serialize (no empty-clobber, no double-migrate). The migrator and the old + // addon load only if a migration actually fires. + ensureV3(graphPath, strPath); + + // The C store opens both files and self-locks around its own init. It owns + // the memfile, string table, and name index. + this.db = new Store(graphPath, strPath); + + // Initial structural sampling + MERW under an exclusive lock (the C ops + // mutate the graph file; withWriteLock syncs on exit). this.withWriteLock(() => { - if (this.nameIndex.size > 0) { - structuralSample(this.gf, 1, 0.85); - computeMerwPsi(this.gf); + if (this.db.entityCount() > 0) { + this.db.structuralSample(1, 0.85); + this.db.computeMerwPsi(0.85, 200, 1e-8); } }); } @@ -436,7 +407,7 @@ export class KnowledgeGraphManager { 'kb.load.top_k': topK, }, (span) => this.withWriteLock(() => { - const result = loadDocument(text, title, this.st, topK); + const result = loadDocument(text, title, this.corpusShim() as never, topK); span.setAttribute('kb.load.chunks', result.stats.chunks); span.setAttribute('kb.load.sentences', result.stats.sentences); span.setAttribute('kb.load.unique_words', result.stats.uniqueWords); @@ -472,18 +443,15 @@ export class KnowledgeGraphManager { private withReadLock(fn: () => T): T { return traced('kb.lock.read', {}, (span) => { const acquireStart = process.hrtime.bigint(); - this.gf.lockShared(); + this.db.lockShared(); const waitMs = Number(process.hrtime.bigint() - acquireStart) / 1e6; span.setAttribute('kb.lock.wait_ms', waitMs); try { - this.gf.refresh(); - this.st.refresh(); - this.rebuildNameIndex(); - this.invalidateTrigramIndexIfStale(); - span.setAttribute('kb.entity_count', this.nameIndex.size); + this.db.refresh(); + span.setAttribute('kb.entity_count', this.db.entityCount()); return fn(); } finally { - this.gf.unlock(); + this.db.unlock(); } }); } @@ -508,133 +476,40 @@ export class KnowledgeGraphManager { private withWriteLock(fn: () => T): T { return traced('kb.lock.write', {}, (span) => { const acquireStart = process.hrtime.bigint(); - this.gf.lockExclusive(); + this.db.lockExclusive(); const waitMs = Number(process.hrtime.bigint() - acquireStart) / 1e6; span.setAttribute('kb.lock.wait_ms', waitMs); try { - this.gf.refresh(); - this.st.refresh(); - this.rebuildNameIndex(); - this.invalidateTrigramIndexIfStale(); - span.setAttribute('kb.entity_count', this.nameIndex.size); + this.db.refresh(); + span.setAttribute('kb.entity_count', this.db.entityCount()); const result = fn(); - this.gf.sync(); - this.st.sync(); - // The mutation we just ran may have shifted entity count or strings - // allocated; refresh the trigram fingerprint so the *next* lock - // acquisition doesn't false-positive an "external" change. The - // mutation methods themselves keep the index hot via incremental - // updates; this is just bookkeeping. - if (this.trigramIndex !== null) { - this.trigramFingerprint = { - entityCount: this.gf.getEntityCount(), - stringsAllocated: this.st.mf.stats().allocated, - }; - } + this.db.sync(); return result; } finally { - this.gf.unlock(); + this.db.unlock(); } }); } - /** Rebuild the in-memory name→offset index from the node log. */ - private rebuildNameIndex(): void { - this.nameIndex.clear(); - const offsets = this.gf.getAllEntityOffsets(); - for (const offset of offsets) { - const rec = this.gf.readEntity(offset); - const name = this.st.get(BigInt(rec.nameId)); - this.nameIndex.set(name, offset); - } - } - - // --- Trigram index helpers ---------------------------------------------- + // --- kb_load corpus shim -------------------------------------------------- /** - * Concatenate an entity's searchable strings (name + type + observations) - * into one indexed text. Same shape as `searchNodes` filters on, so a - * candidate hit means the regex *might* match somewhere on this entity. + * Minimal StringTable-shaped shim for loadDocument's IDF corpus pass: yields + * every entity's name/type/observation strings from the C store. The real + * string table now lives in C; loadDocument only consumes `entries()`. */ - private entityIndexedText(rec: EntityRecord): string { - const parts: string[] = [ - this.st.get(BigInt(rec.nameId)), - this.st.get(BigInt(rec.typeId)), - ]; - if (rec.obs0Id !== 0) parts.push(this.st.get(BigInt(rec.obs0Id))); - if (rec.obs1Id !== 0) parts.push(this.st.get(BigInt(rec.obs1Id))); - return parts.join('\n'); - } - - /** - * Lazy build of the trigram index. Called by `searchNodes` when literals - * are supplied and the index is null. Costs O(total chars) — on a 42 K - * entity KB with mostly-short observations, ~100 ms; bigger KBs scale - * roughly linearly. - * - * MUST be called inside a lock (the manager's withRead/WriteLock). - */ - private buildTrigramIndex(): void { - return traced('kb.trigram.build', {}, (span) => { - const t0 = process.hrtime.bigint(); - const idx = new TrigramIndex(); - const offsets = this.gf.getAllEntityOffsets(); - idx.rebuild((function* (this: KnowledgeGraphManager) { - for (const offset of offsets) { - const rec = this.gf.readEntity(offset); - yield [offset, this.entityIndexedText(rec)] as const; + private corpusShim(): { entries: () => Generator<{ id: bigint; text: string; refcount: number }> } { + const db = this.db; + return { + *entries() { + for (const off of db.listEntities()) { + const r = db.readEntity(off); + yield { id: off, text: r.name, refcount: 1 }; + yield { id: off, text: r.type, refcount: 1 }; + for (const o of r.observations) yield { id: off, text: o, refcount: 1 }; } - }).call(this)); - this.trigramIndex = idx; - this.trigramFingerprint = { - entityCount: this.gf.getEntityCount(), - stringsAllocated: this.st.mf.stats().allocated, - }; - span.setAttribute('kb.entity_count', this.nameIndex.size); - span.setAttribute('kb.trigram.distinct', idx.distinctTrigrams); - span.setAttribute('kb.trigram.build_ms', Number(process.hrtime.bigint() - t0) / 1e6); - }); - } - - /** - * Drop the trigram index if a cross-process change is detected. Called at - * every lock acquisition. Single-process workloads keep the index across - * the entire session; multi-process workloads forfeit the speedup whenever - * another writer wins the race. - */ - private invalidateTrigramIndexIfStale(): void { - if (this.trigramIndex === null) return; - const ec = this.gf.getEntityCount(); - const sa = this.st.mf.stats().allocated; - if (ec !== this.trigramFingerprint.entityCount || sa !== this.trigramFingerprint.stringsAllocated) { - this.trigramIndex = null; - } - } - - /** - * Add an entity to the trigram index, if the index is built. Idempotent. - * Called from mutation methods after the entity record is written. - */ - private trigramAddEntity(offset: bigint, rec: EntityRecord): void { - if (this.trigramIndex === null) return; - this.trigramIndex.add(offset, this.entityIndexedText(rec)); - } - - /** Remove an entity from the trigram index, if the index is built. */ - private trigramRemoveEntity(offset: bigint): void { - if (this.trigramIndex === null) return; - this.trigramIndex.remove(offset); - } - - /** - * Replace an entity's contribution in the trigram index — needed when its - * observations change but the offset stays the same. Equivalent to remove - * + add but skips work if the index is null. - */ - private trigramRefreshEntity(offset: bigint, rec: EntityRecord): void { - if (this.trigramIndex === null) return; - this.trigramIndex.remove(offset); - this.trigramIndex.add(offset, this.entityIndexedText(rec)); + }, + }; } /** Build rank maps from the binary store for pagerank/llmrank sorting. @@ -645,16 +520,16 @@ export class KnowledgeGraphManager { * per-entity rank fields under the active lock. */ private getRankMapsUnlocked(): { structural: Map; walker: Map } { - return traced('kb.rank.read', { 'kb.entity_count': this.nameIndex.size }, () => { + return traced('kb.rank.read', { 'kb.entity_count': this.db.entityCount() }, () => { const structural = new Map(); const walker = new Map(); - const structTotal = this.gf.getStructuralTotal(); - const walkerTotal = this.gf.getWalkerTotal(); + const structTotal = this.db.structuralTotal(); + const walkerTotal = this.db.walkerTotal(); - for (const [name, offset] of this.nameIndex) { - const rec = this.gf.readEntity(offset); - structural.set(name, structTotal > 0n ? Number(rec.structuralVisits) / Number(structTotal) : 0); - walker.set(name, walkerTotal > 0n ? Number(rec.walkerVisits) / Number(walkerTotal) : 0); + for (const offset of this.db.listEntities()) { + const rec = this.db.readEntity(offset); + structural.set(rec.name, structTotal > 0n ? Number(rec.structuralVisits) / Number(structTotal) : 0); + walker.set(rec.name, walkerTotal > 0n ? Number(rec.walkerVisits) / Number(walkerTotal) : 0); } return { structural, walker }; @@ -671,9 +546,9 @@ export class KnowledgeGraphManager { traced('kb.walker.record', { 'kb.walker.count': names.length }, () => { this.withWriteLock(() => { for (const name of names) { - const offset = this.nameIndex.get(name); - if (offset !== undefined) { - this.gf.incrementWalkerVisit(offset); + const offset = this.db.lookup(name); + if (offset !== 0n) { + this.db.incWalkerVisit(offset); } } }); @@ -691,29 +566,23 @@ export class KnowledgeGraphManager { */ resample(): void { this.withWriteLock(() => { - if (this.nameIndex.size === 0) return; + if (this.db.entityCount() === 0) return; traced( 'kb.rank.pagerank', - { 'kb.entity_count': this.nameIndex.size }, - () => structuralSample(this.gf, 1, 0.85), + { 'kb.entity_count': this.db.entityCount() }, + () => this.db.structuralSample(1, 0.85), ); traced( 'kb.rank.merw', - { 'kb.entity_count': this.nameIndex.size }, - () => computeMerwPsi(this.gf), + { 'kb.entity_count': this.db.entityCount() }, + () => this.db.computeMerwPsi(0.85, 200, 1e-8), ); }); } - /** Convert an EntityRecord to the public Entity interface */ - private recordToEntity(rec: EntityRecord): Entity { - const name = this.st.get(BigInt(rec.nameId)); - const entityType = this.st.get(BigInt(rec.typeId)); - const observations: string[] = []; - if (rec.obs0Id !== 0) observations.push(this.st.get(BigInt(rec.obs0Id))); - if (rec.obs1Id !== 0) observations.push(this.st.get(BigInt(rec.obs1Id))); - - const entity: Entity = { name, entityType, observations }; + /** Convert a native entity record to the public Entity interface */ + private recordToEntity(rec: NativeEntity): Entity { + const entity: Entity = { name: rec.name, entityType: rec.type, observations: rec.observations }; const mtime = Number(rec.mtime); const obsMtime = Number(rec.obsMtime); if (mtime > 0) entity.mtime = mtime; @@ -723,24 +592,18 @@ export class KnowledgeGraphManager { /** Get all entities as Entity objects (preserves node log order = insertion order) */ private getAllEntities(): Entity[] { - const offsets = this.gf.getAllEntityOffsets(); - return offsets.map(o => this.recordToEntity(this.gf.readEntity(o))); + return this.db.listEntities().map(o => this.recordToEntity(this.db.readEntity(o))); } /** Get all relations by scanning adjacency lists (forward edges only to avoid duplication) */ private getAllRelations(): Relation[] { const relations: Relation[] = []; - const offsets = this.gf.getAllEntityOffsets(); - for (const offset of offsets) { - const rec = this.gf.readEntity(offset); - const fromName = this.st.get(BigInt(rec.nameId)); - const edges = this.gf.getEdges(offset); - for (const edge of edges) { + for (const offset of this.db.listEntities()) { + const fromName = this.db.entityName(offset); + for (const edge of this.db.edges(offset)) { if (edge.direction !== DIR_FORWARD) continue; - const targetRec = this.gf.readEntity(edge.targetOffset); - const toName = this.st.get(BigInt(targetRec.nameId)); - const relationType = this.st.get(BigInt(edge.relTypeId)); - const r: Relation = { from: fromName, to: toName, relationType }; + const toName = this.db.entityName(edge.target); + const r: Relation = { from: fromName, to: toName, relationType: edge.relType }; const mtime = Number(edge.mtime); if (mtime > 0) r.mtime = mtime; relations.push(r); @@ -775,9 +638,9 @@ export class KnowledgeGraphManager { const newEntities: Entity[] = []; for (const e of entities) { - const existingOffset = this.nameIndex.get(e.name); - if (existingOffset !== undefined) { - const existing = this.recordToEntity(this.gf.readEntity(existingOffset)); + const existingOffset = this.db.lookup(e.name); + if (existingOffset !== 0n) { + const existing = this.recordToEntity(this.db.readEntity(existingOffset)); const sameType = existing.entityType === e.entityType; const sameObs = existing.observations.length === e.observations.length && existing.observations.every((o, i) => o === e.observations[i]); @@ -785,27 +648,11 @@ export class KnowledgeGraphManager { throw new Error(`Entity "${e.name}" already exists with different data (type: "${existing.entityType}" vs "${e.entityType}", observations: ${existing.observations.length} vs ${e.observations.length})`); } - const obsMtime = e.observations.length > 0 ? now : 0n; - const rec = this.gf.createEntity(e.name, e.entityType, now, obsMtime); - + const offset = this.db.createEntity(e.name, e.entityType, now); for (const obs of e.observations) { - this.gf.addObservation(rec.offset, obs, now); - } - - if (e.observations.length > 0) { - const updated = this.gf.readEntity(rec.offset); - updated.mtime = now; - updated.obsMtime = now; - this.gf.updateEntity(updated); + this.db.addObservation(offset, obs, now); } - this.nameIndex.set(e.name, rec.offset); - - // Keep the trigram index hot if it was already built. The index keys - // by entity offset and contains name + type + observations, so we - // index AFTER the observation writes have landed. - this.trigramAddEntity(rec.offset, this.gf.readEntity(rec.offset)); - const newEntity: Entity = { ...e, mtime: Number(now), @@ -822,50 +669,22 @@ export class KnowledgeGraphManager { return this.withWriteLock(() => { const now = BigInt(Date.now()); const newRelations: Relation[] = []; - const fromOffsets = new Set(); for (const r of relations) { - const fromOffset = this.nameIndex.get(r.from); - const toOffset = this.nameIndex.get(r.to); - if (fromOffset === undefined || toOffset === undefined) continue; - - const existingEdges = this.gf.getEdges(fromOffset); - const relTypeId = Number(this.st.find(r.relationType) ?? -1n); - const isDuplicate = existingEdges.some(e => - e.direction === DIR_FORWARD && - e.targetOffset === toOffset && - e.relTypeId === relTypeId + const fromOffset = this.db.lookup(r.from); + const toOffset = this.db.lookup(r.to); + if (fromOffset === 0n || toOffset === 0n) continue; + + const isDuplicate = this.db.edges(fromOffset).some(e => + e.direction === DIR_FORWARD && e.target === toOffset && e.relType === r.relationType ); if (isDuplicate) continue; - const rTypeId = Number(this.st.intern(r.relationType)); - const forwardEntry: AdjEntry = { - targetOffset: toOffset, - direction: DIR_FORWARD, - relTypeId: rTypeId, - mtime: now, - }; - this.gf.addEdge(fromOffset, forwardEntry); - - const rTypeId2 = Number(this.st.intern(r.relationType)); - const backwardEntry: AdjEntry = { - targetOffset: fromOffset, - direction: DIR_BACKWARD, - relTypeId: rTypeId2, - mtime: now, - }; - this.gf.addEdge(toOffset, backwardEntry); - - fromOffsets.add(fromOffset); + // C owns the bidirectional edges + relType interning/refcounts. + this.db.createRelation(fromOffset, toOffset, r.relationType, now); newRelations.push({ ...r, mtime: Number(now) }); } - for (const offset of fromOffsets) { - const rec = this.gf.readEntity(offset); - rec.mtime = now; - this.gf.updateEntity(rec); - } - return newRelations; }); } @@ -875,8 +694,8 @@ export class KnowledgeGraphManager { const results: { entityName: string; addedObservations: string[] }[] = []; for (const o of observations) { - const offset = this.nameIndex.get(o.entityName); - if (offset === undefined) { + const offset = this.db.lookup(o.entityName); + if (offset === 0n) { throw new Error(`Entity with name ${o.entityName} not found`); } @@ -886,11 +705,7 @@ export class KnowledgeGraphManager { } } - const rec = this.gf.readEntity(offset); - const existingObs: string[] = []; - if (rec.obs0Id !== 0) existingObs.push(this.st.get(BigInt(rec.obs0Id))); - if (rec.obs1Id !== 0) existingObs.push(this.st.get(BigInt(rec.obs1Id))); - + const existingObs = this.db.readEntity(offset).observations; const newObservations = o.contents.filter(content => !existingObs.includes(content)); if (existingObs.length + newObservations.length > 2) { @@ -899,12 +714,7 @@ export class KnowledgeGraphManager { const now = BigInt(Date.now()); for (const obs of newObservations) { - this.gf.addObservation(offset, obs, now); - } - - // Refresh trigram index entry — observations changed. - if (newObservations.length > 0) { - this.trigramRefreshEntity(offset, this.gf.readEntity(offset)); + this.db.addObservation(offset, obs, now); } results.push({ entityName: o.entityName, addedObservations: newObservations }); @@ -917,22 +727,11 @@ export class KnowledgeGraphManager { async deleteEntities(entityNames: string[]): Promise { this.withWriteLock(() => { for (const name of entityNames) { - const offset = this.nameIndex.get(name); - if (offset === undefined) continue; - - const edges = this.gf.getEdges(offset); - for (const edge of edges) { - const reverseDir = edge.direction === DIR_FORWARD ? DIR_BACKWARD : DIR_FORWARD; - this.gf.removeEdge(edge.targetOffset, offset, edge.relTypeId, reverseDir); - this.st.release(BigInt(edge.relTypeId)); - } - - // Drop the trigram entry BEFORE freeing — once the entity record is - // freed its strings may be released and the offset may be recycled. - this.trigramRemoveEntity(offset); - - this.gf.deleteEntity(offset); - this.nameIndex.delete(name); + const offset = this.db.lookup(name); + if (offset === 0n) continue; + // C deletes the record + adjacency, drops mirror edges, and releases + // every string ref (name/type/obs + relType per edge + mirror). + this.db.deleteEntity(offset); } }); } @@ -941,15 +740,12 @@ export class KnowledgeGraphManager { this.withWriteLock(() => { const now = BigInt(Date.now()); for (const d of deletions) { - const offset = this.nameIndex.get(d.entityName); - if (offset === undefined) continue; + const offset = this.db.lookup(d.entityName); + if (offset === 0n) continue; for (const obs of d.observations) { - this.gf.removeObservation(offset, obs, now); + this.db.removeObservation(offset, obs, now); } - - // Refresh trigram entry — observations may have changed. - this.trigramRefreshEntity(offset, this.gf.readEntity(offset)); } }); } @@ -957,18 +753,11 @@ export class KnowledgeGraphManager { async deleteRelations(relations: Relation[]): Promise { this.withWriteLock(() => { for (const r of relations) { - const fromOffset = this.nameIndex.get(r.from); - const toOffset = this.nameIndex.get(r.to); - if (fromOffset === undefined || toOffset === undefined) continue; - - const relTypeId = this.st.find(r.relationType); - if (relTypeId === null) continue; - - const removedForward = this.gf.removeEdge(fromOffset, toOffset, Number(relTypeId), DIR_FORWARD); - if (removedForward) this.st.release(relTypeId); - - const removedBackward = this.gf.removeEdge(toOffset, fromOffset, Number(relTypeId), DIR_BACKWARD); - if (removedBackward) this.st.release(relTypeId); + const fromOffset = this.db.lookup(r.from); + const toOffset = this.db.lookup(r.to); + if (fromOffset === 0n || toOffset === 0n) continue; + // C removes both directed edges and releases the two relType refs. + this.db.deleteRelation(fromOffset, toOffset, r.relationType); } }); } @@ -989,18 +778,14 @@ export class KnowledgeGraphManager { sortDir?: SortDirection, direction: 'forward' | 'backward' | 'any' = 'forward', ): Promise { - let regex: RegExp; + // Validate the pattern with JS RegExp semantics so callers still get the + // "Invalid regex pattern" contract; the actual match runs in C (POSIX ERE). try { - regex = new RegExp(query, 'i'); + new RegExp(query, 'i'); } catch { throw new Error(`Invalid regex pattern: ${query}`); } - // Cox-style trigram query extraction: parse the regex AST, propagate - // match-info bottom-up, produce a boolean trigram expression. Returns - // null when the regex collapses to no useful constraint (e.g., `.*`). - const trigramQuery = buildTrigramQuery(query); - return traced( 'kb.search_nodes', { @@ -1009,41 +794,7 @@ export class KnowledgeGraphManager { ...(sortBy ? { 'kb.search.sort_by': sortBy } : {}), }, (span) => this.withReadLock(() => { - let filteredEntities: Entity[]; - let scannedCount: number; - let usedTrigram = false; - - if (trigramQuery !== null) { - if (this.trigramIndex === null) this.buildTrigramIndex(); - const candidateOffsets = this.trigramIndex!.evaluateQuery(trigramQuery); - if (candidateOffsets !== null) { - usedTrigram = true; - const out: Entity[] = []; - for (const offset of candidateOffsets) { - const e = this.recordToEntity(this.gf.readEntity(offset)); - if (regex.test(e.name) || regex.test(e.entityType) || e.observations.some(o => regex.test(o))) { - out.push(e); - } - } - filteredEntities = out; - scannedCount = candidateOffsets.length; - } else { - // Query collapsed to `all` after evaluation — full scan. - const allEntities = this.getAllEntities(); - filteredEntities = allEntities.filter(e => - regex.test(e.name) || regex.test(e.entityType) || e.observations.some(o => regex.test(o)) - ); - scannedCount = allEntities.length; - } - } else { - // Regex isn't trigram-extractable (e.g. `.*`, `\D+`) — full scan. - const allEntities = this.getAllEntities(); - filteredEntities = allEntities.filter(e => - regex.test(e.name) || regex.test(e.entityType) || e.observations.some(o => regex.test(o)) - ); - scannedCount = allEntities.length; - } - + const filteredEntities = this.db.search(query).map(o => this.recordToEntity(this.db.readEntity(o))); const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); const allRelations = this.getAllRelations(); @@ -1053,10 +804,8 @@ export class KnowledgeGraphManager { return filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to); }); - // Aggregate scan/match stats — the LLM-visible knobs that explain why - // a query was slow: how big the haystack is and how big the result. - span.setAttribute('kb.search.used_trigram', usedTrigram); - span.setAttribute('kb.search.scanned.entities', scannedCount); + span.setAttribute('kb.search.used_trigram', false); + span.setAttribute('kb.search.scanned.entities', this.db.entityCount()); span.setAttribute('kb.search.scanned.relations', allRelations.length); span.setAttribute('kb.search.matched.entities', filteredEntities.length); span.setAttribute('kb.search.matched.relations', filteredRelations.length); @@ -1073,24 +822,24 @@ export class KnowledgeGraphManager { async openNodes(names: string[], direction: 'forward' | 'backward' | 'any' = 'forward'): Promise { return this.withReadLock(() => { const filteredEntities: Entity[] = []; + const offsetByName = new Map(); for (const name of names) { - const offset = this.nameIndex.get(name); - if (offset === undefined) continue; - filteredEntities.push(this.recordToEntity(this.gf.readEntity(offset))); + const offset = this.db.lookup(name); + if (offset === 0n) continue; + filteredEntities.push(this.recordToEntity(this.db.readEntity(offset))); + offsetByName.set(name, offset); } const filteredEntityNames = new Set(filteredEntities.map(e => e.name)); const filteredRelations: Relation[] = []; for (const name of filteredEntityNames) { - const offset = this.nameIndex.get(name)!; - const edges = this.gf.getEdges(offset); - for (const edge of edges) { + const offset = offsetByName.get(name)!; + for (const edge of this.db.edges(offset)) { if (edge.direction !== DIR_FORWARD && edge.direction !== DIR_BACKWARD) continue; - const targetRec = this.gf.readEntity(edge.targetOffset); - const targetName = this.st.get(BigInt(targetRec.nameId)); - const relationType = this.st.get(BigInt(edge.relTypeId)); + const targetName = this.db.entityName(edge.target); + const relationType = edge.relType; const mtime = Number(edge.mtime); if (edge.direction === DIR_FORWARD) { @@ -1127,61 +876,27 @@ export class KnowledgeGraphManager { ...(sortBy ? { 'kb.traversal.sort_by': sortBy } : {}), }, (span) => this.withReadLock(() => { - const startOffset = this.nameIndex.get(entityName); - if (startOffset === undefined) { + const startOffset = this.db.lookup(entityName); + if (startOffset === 0n) { span.setAttribute('kb.traversal.start_found', false); return []; } span.setAttribute('kb.traversal.start_found', true); - const visited = new Set(); - const neighborNames = new Set(); - let edgesScanned = 0; - - const traverse = (currentName: string, currentDepth: number): void => { - if (currentDepth > depth || visited.has(currentName)) return; - visited.add(currentName); - - const offset = this.nameIndex.get(currentName); - if (offset === undefined) return; - - const edges = this.gf.getEdges(offset); - for (const edge of edges) { - edgesScanned++; - if (direction === 'forward' && edge.direction !== DIR_FORWARD) continue; - if (direction === 'backward' && edge.direction !== DIR_BACKWARD) continue; - - const targetRec = this.gf.readEntity(edge.targetOffset); - const neighborName = this.st.get(BigInt(targetRec.nameId)); - neighborNames.add(neighborName); - - if (currentDepth < depth) { - traverse(neighborName, currentDepth + 1); - } - } - }; - - traverse(entityName, 0); - neighborNames.delete(entityName); - - const neighbors: Neighbor[] = Array.from(neighborNames).map(name => { - const offset = this.nameIndex.get(name); - if (!offset) return { name }; - const rec = this.gf.readEntity(offset); + // C BFS returns neighbor offsets within `depth` hops, excluding start. + // The old TS semantics were one hop deeper (depth=0 returned immediate + // neighbors), so request depth+1 from C to match. + const neighbors: Neighbor[] = this.db.neighbors(startOffset, depth + 1, direction).map(off => { + const rec = this.db.readEntity(off); const mtime = Number(rec.mtime); const obsMtime = Number(rec.obsMtime); - const n: Neighbor = { name }; + const n: Neighbor = { name: rec.name }; if (mtime > 0) n.mtime = mtime; if (obsMtime > 0) n.obsMtime = obsMtime; return n; }); - // Aggregate traversal stats: the size of the BFS frontier the - // resulting neighbor set, and how many edges we walked through. Cheap - // to compute and explains slow calls without per-step span fanout. - span.setAttribute('kb.traversal.visited_count', visited.size); span.setAttribute('kb.traversal.neighbor_count', neighbors.length); - span.setAttribute('kb.traversal.edges_scanned', edgesScanned); const rankMaps = this.getRankMapsUnlocked(); return sortNeighbors(neighbors, sortBy, sortDir, rankMaps); @@ -1246,174 +961,67 @@ export class KnowledgeGraphManager { // `maxDepth` arg. DFS gave the first path the recursion discovered, // not the shortest. (span) => this.withReadLock(() => { - let edgesScanned = 0; - let nodesExpanded = 0; + const budgetBytes = findPathBudgetBytes(); - // from === to: trivial 0-hop path; no edges to report. Matches - // the pre-rewrite behavior of `dfs(from, from, [], 0) → []` and - // is the only path-length-zero case that counts as - // `targetReached`. + // from === to: trivial 0-hop path; no edges. Only path-length-zero + // case that counts as targetReached. if (fromEntity === toEntity) { - span.setAttribute('kb.traversal.nodes_expanded', 0); - span.setAttribute('kb.traversal.edges_scanned', 0); span.setAttribute('kb.traversal.path_length', 0); span.setAttribute('kb.traversal.path_found', true); span.setAttribute('kb.traversal.target_reached', true); - span.setAttribute('kb.traversal.budget_bytes', findPathBudgetBytes()); - span.setAttribute('kb.traversal.bytes_used', 0); - span.setAttribute('kb.traversal.budget_exhausted', false); - return { path: [], targetReached: true, budgetExhausted: false, budgetBytes: findPathBudgetBytes() }; + return { path: [], targetReached: true, budgetExhausted: false, budgetBytes }; } - // Per-call memory budget. We track `bytesUsed` as the exact sum - // of *our* allocations into BFS data structures: UTF-8 payload - // bytes of strings we materialize, plus V8's documented - // container-slot overhead. No `process.memoryUsage()` — that - // would conflate this BFS's footprint with OTel spans, request - // handler state, GC fluctuations, anything else in the isolate. - // See the FIND_PATH_MEMORY_FRACTION block above for the recipe. - const budgetBytes = findPathBudgetBytes(); - let bytesUsed = Buffer.byteLength(fromEntity, 'utf-8') + V8_SET_ENTRY; // `visited` start - let budgetExhausted = false; - - // BFS bookkeeping. `parent` maps each discovered node to the - // (predecessor, edge) we discovered it through, used to - // reconstruct the path on success. `visited` is populated *on - // enqueue* (not on dequeue) so each node enters the queue at - // most once — eliminates the O(b^d) backtrack explosion. - const parent = new Map(); - const visited = new Set([fromEntity]); - let frontier: string[] = [fromEntity]; - let depth = 0; - let found = false; - // Tracked alongside `parent`: the most-recent name added. Because - // BFS expands layer-by-layer, the last entry recorded into - // `parent` is at the deepest layer reached. This is our anchor - // for the SMA*-style best-effort return when target isn't - // reached (β contract): with no heuristic on graph distance, - // "deepest BFS-reached" is the defensible "farthest progress - // toward target" — and the natural retry-from anchor. - let farthestDiscovered: string | undefined; - - // We expand nodes at distance `depth` to discover nodes at - // distance `depth + 1`. The loop runs while `depth < maxDepth` - // so the deepest discovery is at level `maxDepth` (i.e. a - // path of `maxDepth` relations). This matches the predecessor: - // pre-rewrite DFS pruned with `depth > maxDepth`, so paths of - // exactly `maxDepth` hops were accepted. - outer: while (frontier.length > 0 && depth < maxDepth) { - const nextFrontier: string[] = []; - for (const current of frontier) { - nodesExpanded++; - const offset = this.nameIndex.get(current); - if (offset === undefined) continue; - - const edges = this.gf.getEdges(offset); - for (const edge of edges) { - edgesScanned++; - if (direction === 'forward' && edge.direction !== DIR_FORWARD) continue; - if (direction === 'backward' && edge.direction !== DIR_BACKWARD) continue; - - const targetRec = this.gf.readEntity(edge.targetOffset); - const nextName = this.st.get(BigInt(targetRec.nameId)); - if (visited.has(nextName)) continue; - - const relationType = this.st.get(BigInt(edge.relTypeId)); - const mtime = Number(edge.mtime); - const rel: Relation = edge.direction === DIR_FORWARD - ? { from: current, to: nextName, relationType } - : { from: nextName, to: current, relationType }; - if (mtime > 0) rel.mtime = mtime; - - visited.add(nextName); - parent.set(nextName, { fromName: current, edge: rel }); - farthestDiscovered = nextName; // see β-contract notes above - - // Exact accounting for this discovery, no estimates: - // visited.add(nextName): - // + UTF-8 bytes of nextName + V8_SET_ENTRY - // parent.set(nextName, {fromName: current, edge: rel}): - // + V8_MAP_ENTRY (map slot) - // + V8_OBJECT_HEADER ({fromName,edge} wrapper) - // + V8_OBJECT_HEADER (Relation object rel) - // + UTF-8 bytes of relationType (rel.relationType is a new string) - // Aliased: rel.from / rel.to / fromName reference strings - // already counted in `visited` — not double-counted. - bytesUsed += Buffer.byteLength(nextName, 'utf-8') - + V8_SET_ENTRY - + V8_MAP_ENTRY - + V8_OBJECT_HEADER - + V8_OBJECT_HEADER - + Buffer.byteLength(relationType, 'utf-8'); - - if (nextName === toEntity) { - found = true; - break outer; - } - nextFrontier.push(nextName); - - if (bytesUsed >= budgetBytes) { - budgetExhausted = true; - break outer; - } - } - } - depth++; - frontier = nextFrontier; + const fromOffset = this.db.lookup(fromEntity); + const toOffset = this.db.lookup(toEntity); + if (fromOffset === 0n || toOffset === 0n) { + span.setAttribute('kb.traversal.path_found', false); + span.setAttribute('kb.traversal.target_reached', false); + return { path: [], targetReached: false, budgetExhausted: false, budgetBytes }; } - // Path reconstruction. Two cases: - // - // found=true → walk parent-pointers back from `toEntity`, - // producing the (shortest) path BFS found. - // - // found=false → β-contract best-effort: walk parent-pointers - // back from `farthestDiscovered` (the deepest - // node BFS expanded). This is the path the - // caller can use as an anchor to continue from. - // If `farthestDiscovered` is undefined the BFS - // expanded no edges at all — `fromEntity` was - // missing from the name index or had no - // direction-matching outgoing edges. Return an - // empty path with `targetReached=false` in that - // case; nothing useful to anchor on. - let path: Relation[] = []; - const reconstructFrom = found ? toEntity : farthestDiscovered; - if (reconstructFrom !== undefined) { - const rels: Relation[] = []; - let cur = reconstructFrom; - while (cur !== fromEntity) { - const p = parent.get(cur); - if (!p) break; // unreachable defensively; parent chain is total under BFS invariant - rels.push(p.edge); - cur = p.fromName; - } - path = rels.reverse(); + // C BFS: shortest path to target, or a best-effort path to the + // farthest-discovered node when the target isn't reached (β-contract). + // The byte budget bounds the C BFS just as it bounded the old JS BFS; + // KB_FIND_PATH_BUDGET_BYTES flows in via findPathBudgetBytes(). + const res = this.db.findPath(fromOffset, toOffset, maxDepth, direction, BigInt(budgetBytes)); + const found = res.targetReached; + const nodePath = res.path; + + const path: Relation[] = []; + for (let i = 0; i + 1 < nodePath.length; i++) { + const cur = nodePath[i]; + const next = nodePath[i + 1]; + const e = this.db.edges(cur).find(ed => ed.target === next && ( + direction === 'forward' ? ed.direction === DIR_FORWARD : + direction === 'backward' ? ed.direction === DIR_BACKWARD : + (ed.direction === DIR_FORWARD || ed.direction === DIR_BACKWARD) + )); + if (!e) continue; + const curName = this.db.entityName(cur); + const nextName = this.db.entityName(next); + const rel: Relation = e.direction === DIR_FORWARD + ? { from: curName, to: nextName, relationType: e.relType } + : { from: nextName, to: curName, relationType: e.relType }; + const mtime = Number(e.mtime); + if (mtime > 0) rel.mtime = mtime; + path.push(rel); } - span.setAttribute('kb.traversal.nodes_expanded', nodesExpanded); - span.setAttribute('kb.traversal.edges_scanned', edgesScanned); + const farthestDiscovered = (!found && res.farthest !== 0n) + ? this.db.entityName(res.farthest) : undefined; + span.setAttribute('kb.traversal.path_length', path.length); span.setAttribute('kb.traversal.path_found', found); span.setAttribute('kb.traversal.target_reached', found); - // `budget_bytes` is the per-call cap derived from RAM (or env override). - // `bytes_used` is the *exact* sum we computed during BFS — string - // payload (UTF-8) + V8 container-slot overhead derived from V8's - // OrderedHashSet/OrderedHashMap/JSObject layouts. No estimate - // multiplier; if these don't match the V8 footprint within tens of - // percent we adjust the structural constants, not a fudge factor. - // `budget_exhausted` lights up on the rare call that hit the cap. span.setAttribute('kb.traversal.budget_bytes', budgetBytes); - span.setAttribute('kb.traversal.bytes_used', bytesUsed); - span.setAttribute('kb.traversal.budget_exhausted', budgetExhausted); - if (!found && farthestDiscovered !== undefined) { - span.setAttribute('kb.traversal.farthest_discovered', farthestDiscovered); - } + span.setAttribute('kb.traversal.budget_exhausted', res.budgetExhausted); + if (farthestDiscovered !== undefined) span.setAttribute('kb.traversal.farthest_discovered', farthestDiscovered); return { path, targetReached: found, - budgetExhausted, - farthestDiscovered: !found ? farthestDiscovered : undefined, + budgetExhausted: res.budgetExhausted, + farthestDiscovered, budgetBytes, }; }), @@ -1565,96 +1173,21 @@ export class KnowledgeGraphManager { 'kb.walker.seeded': seed !== undefined, }, (span) => this.withReadLock(() => { - const startOffset = this.nameIndex.get(start); - if (!startOffset) { + const startOffset = this.db.lookup(start); + if (startOffset === 0n) { throw new Error(`Start entity not found: ${start}`); } - // Create seeded RNG if seed provided - let rngState = seed ? this.hashSeed(seed) : null; - const random = (): number => { - if (rngState !== null) { - rngState ^= rngState << 13; - rngState ^= rngState >>> 17; - rngState ^= rngState << 5; - return (rngState >>> 0) / 0xFFFFFFFF; - } else { - return randomBytes(4).readUInt32BE() / 0xFFFFFFFF; - } - }; - - const pathNames: string[] = [start]; - let current = start; - let edgesScanned = 0; - let truncated = false; - - for (let i = 0; i < depth; i++) { - const offset = this.nameIndex.get(current); - if (!offset) { truncated = true; break; } - - const edges = this.gf.getEdges(offset); - const candidates: { name: string; psi: number }[] = []; - - for (const edge of edges) { - edgesScanned++; - if (direction === 'forward' && edge.direction !== DIR_FORWARD) continue; - if (direction === 'backward' && edge.direction !== DIR_BACKWARD) continue; - - const targetRec = this.gf.readEntity(edge.targetOffset); - const neighborName = this.st.get(BigInt(targetRec.nameId)); - if (neighborName !== current && this.nameIndex.has(neighborName)) { - candidates.push({ name: neighborName, psi: targetRec.psi }); - } - } - - // Deduplicate: keep max psi per name (multiple edge types to same target) - const byName = new Map(); - for (const c of candidates) { - const existing = byName.get(c.name); - if (existing === undefined || c.psi > existing) { - byName.set(c.name, c.psi); - } - } - - if (byName.size === 0) { truncated = true; break; } - - const neighborArr = Array.from(byName.entries()); - - // Compute total ψ once; both modes need it (uniform skips it, but - // we still want to know whether ψ is populated for telemetry). - let totalPsi = 0; - for (const [, psi] of neighborArr) totalPsi += psi; - - let chosen: string; - if (mode === 'merw' && totalPsi > 0) { - // MERW-weighted sampling: probability proportional to ψ_j. - // (The ψ_i denominator is constant for all neighbors and - // cancels in normalization.) - const r = random() * totalPsi; - let cumulative = 0; - chosen = neighborArr[neighborArr.length - 1][0]; // fallback - for (const [name, psi] of neighborArr) { - cumulative += psi; - if (r <= cumulative) { - chosen = name; - break; - } - } - } else { - // mode === 'uniform', or MERW with ψ not yet computed (all zero). - // Plain uniform sampling over the deduplicated neighbor set. - const idx = Math.floor(random() * neighborArr.length); - chosen = neighborArr[idx][0]; - } - - current = chosen; - pathNames.push(current); - } + // Seeded walk: hash the string seed to a u64 the C RNG can use. A + // seed of 0 means "use the global RNG" (unseeded), so hashSeed (never + // 0) keeps seeded walks reproducible. + const seedU64 = seed !== undefined ? BigInt(this.hashSeed(seed) >>> 0) : 0n; + const pathOffsets = this.db.randomWalk(startOffset, depth, direction, mode === 'merw', seedU64); + const pathNames = pathOffsets.map(o => this.db.entityName(o)); span.setAttribute('kb.walker.steps_taken', pathNames.length - 1); - span.setAttribute('kb.walker.edges_scanned', edgesScanned); - span.setAttribute('kb.walker.truncated', truncated); - return { entity: current, path: pathNames }; + span.setAttribute('kb.walker.truncated', pathNames.length - 1 < depth); + return { entity: pathNames[pathNames.length - 1], path: pathNames }; }), ); } @@ -1723,35 +1256,18 @@ export class KnowledgeGraphManager { const now = BigInt(Date.now()); const ctxId = randomBytes(12).toString('hex'); - const obsMtime = observations.length > 0 ? now : 0n; - const rec = this.gf.createEntity(ctxId, 'Thought', now, obsMtime); + const offset = this.db.createEntity(ctxId, 'Thought', now); for (const obs of observations) { - this.gf.addObservation(rec.offset, obs, now); - } - if (observations.length > 0) { - const updated = this.gf.readEntity(rec.offset); - updated.mtime = now; - updated.obsMtime = now; - this.gf.updateEntity(updated); + this.db.addObservation(offset, obs, now); } - this.nameIndex.set(ctxId, rec.offset); if (previousCtxId) { - const prevOffset = this.nameIndex.get(previousCtxId); - if (prevOffset !== undefined) { - const prevRec = this.gf.readEntity(prevOffset); - prevRec.mtime = now; - this.gf.updateEntity(prevRec); - - const followsTypeId = Number(this.st.intern('follows')); - this.gf.addEdge(prevOffset, { targetOffset: rec.offset, direction: DIR_FORWARD, relTypeId: followsTypeId, mtime: now }); - const followsTypeId2 = Number(this.st.intern('follows')); - this.gf.addEdge(rec.offset, { targetOffset: prevOffset, direction: DIR_BACKWARD, relTypeId: followsTypeId2, mtime: now }); - - const precededByTypeId = Number(this.st.intern('preceded_by')); - this.gf.addEdge(rec.offset, { targetOffset: prevOffset, direction: DIR_FORWARD, relTypeId: precededByTypeId, mtime: now }); - const precededByTypeId2 = Number(this.st.intern('preceded_by')); - this.gf.addEdge(prevOffset, { targetOffset: rec.offset, direction: DIR_BACKWARD, relTypeId: precededByTypeId2, mtime: now }); + const prevOffset = this.db.lookup(previousCtxId); + if (prevOffset !== 0n) { + // prev --follows--> new, and new --preceded_by--> prev. C creates + // both directed edges per relation and owns the refcounts. + this.db.createRelation(prevOffset, offset, 'follows', now); + this.db.createRelation(offset, prevOffset, 'preceded_by', now); } } @@ -1761,8 +1277,7 @@ export class KnowledgeGraphManager { /** Close the underlying binary store files */ close(): void { - this.gf.close(); - this.st.close(); + this.db.close(); } } @@ -2199,7 +1714,7 @@ The file MUST be plaintext (.txt, .tex, .md, source code, etc.). For PDFs, use p return { content: [{ type: "text", - text: `No matches for ${JSON.stringify(query)}. search_nodes uses regex (case-insensitive), not natural language.${suggestion} You can also browse with get_entities_by_type, get_neighbors, or random_walk.`, + text: `No matches for ${JSON.stringify(query)}. search_nodes uses POSIX Extended Regular Expressions (ERE), case-sensitive — not natural language, and not JS/PCRE regex (use [0-9] not \\d, [[:alpha:]] not \\w; no lookahead or backreferences).${suggestion} You can also browse with get_entities_by_type, get_neighbors, or random_walk.`, }], isError: true, }; diff --git a/src/memoryfile.ts b/src/memoryfile.ts index 22cb0c6..1d81342 100644 --- a/src/memoryfile.ts +++ b/src/memoryfile.ts @@ -42,7 +42,17 @@ function findNative(): string { throw new Error('Could not find native memoryfile.node — was the C addon built? Run: node-gyp rebuild'); } -const native: NativeMemoryFile = require(findNative()); +// Lazy-load the native addon: importing this module (e.g. transitively via the +// migrator) must NOT require memoryfile.node to be present — only constructing +// a MemoryFile does. This lets a fresh v3 deploy run without the old addon +// unless an actual v1/v2 migration fires. +let _native: NativeMemoryFile | undefined; +const native: NativeMemoryFile = new Proxy({} as NativeMemoryFile, { + get(_t, prop: string) { + if (!_native) _native = require(findNative()) as NativeMemoryFile; + return (_native as unknown as Record)[prop]; + }, +}); export class MemoryFile { private handle: unknown; diff --git a/src/merw.ts b/src/merw.ts deleted file mode 100644 index 7a63606..0000000 --- a/src/merw.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Maximum Entropy Random Walk (MERW) — dominant eigenvector computation - * via power iteration on the graph's adjacency matrix. - * - * MERW transition probabilities: S_ij = (A_ij / λ) * (ψ_j / ψ_i) - * Stationary distribution: ρ_i = ψ_i² / ‖ψ‖₂² - * - * We compute ψ (the dominant right eigenvector of A) using sparse power - * iteration directly on the GraphFile adjacency lists. No dense matrix - * is ever constructed. - * - * For directed graphs that may not be strongly connected, we add - * teleportation damping (like PageRank): at each step, follow an edge - * with probability `alpha`, or jump to a uniform random node with - * probability `(1 - alpha)`. This guarantees convergence to a unique - * positive eigenvector. - */ - -import { type GraphFile, DIR_FORWARD } from './graphfile.js'; - -const DEFAULT_ALPHA = 0.85; -const DEFAULT_MAX_ITER = 200; -const DEFAULT_TOL = 1e-8; - -/** - * Compute the dominant eigenvector of the (damped) adjacency matrix - * via power iteration and write ψ_i into each entity record. - * - * Warm-starts from the ψ values already stored in the entity records. - * New nodes (psi === 0) are seeded with the mean of existing values. - * On a fresh graph (all zeros), falls back to uniform initialization. - * - * @param gf GraphFile to operate on - * @param alpha Damping factor (probability of following an edge). Default 0.85. - * @param maxIter Maximum iterations. Default 200. - * @param tol Convergence tolerance (L2 norm of change). Default 1e-8. - * @returns Number of iterations performed. - */ -export function computeMerwPsi( - gf: GraphFile, - alpha: number = DEFAULT_ALPHA, - maxIter: number = DEFAULT_MAX_ITER, - tol: number = DEFAULT_TOL, -): number { - const offsets = gf.getAllEntityOffsets(); - const n = offsets.length; - if (n === 0) return 0; - - // Build offset → index map for O(1) lookup - const indexMap = new Map(); - for (let i = 0; i < n; i++) { - indexMap.set(offsets[i], i); - } - - // Build sparse adjacency: for each node, list of forward neighbor indices - const adj: number[][] = new Array(n); - for (let i = 0; i < n; i++) { - const edges = gf.getEdges(offsets[i]); - const neighbors: number[] = []; - for (const e of edges) { - if (e.direction !== DIR_FORWARD) continue; - const j = indexMap.get(e.targetOffset); - if (j !== undefined) neighbors.push(j); - } - adj[i] = neighbors; - } - - // Warm-start: read existing ψ from entity records - let psi = new Float64Array(n); - let hasWarm = false; - let warmSum = 0; - let warmCount = 0; - - for (let i = 0; i < n; i++) { - const val = gf.getPsi(offsets[i]); - psi[i] = val; - if (val > 0) { - hasWarm = true; - warmSum += val; - warmCount++; - } - } - - if (hasWarm) { - // Seed new/zero nodes with the mean of existing nonzero values - const mean = warmSum / warmCount; - for (let i = 0; i < n; i++) { - if (psi[i] <= 0) psi[i] = mean; - } - } else { - // Cold start: uniform - const uniform = 1.0 / Math.sqrt(n); - psi.fill(uniform); - } - - // Normalize initial vector to unit L2 - let initNorm = 0; - for (let i = 0; i < n; i++) initNorm += psi[i] * psi[i]; - initNorm = Math.sqrt(initNorm); - if (initNorm > 0) { - for (let i = 0; i < n; i++) psi[i] /= initNorm; - } - - let psiNext = new Float64Array(n); - - const teleport = (1.0 - alpha) / n; - - let iter = 0; - for (iter = 0; iter < maxIter; iter++) { - // Matrix-vector multiply: psiNext = alpha * A * psi + (1-alpha)/n * sum(psi) - // Since ψ is normalized, sum(psi) components contribute uniformly. - // For the adjacency multiply, A_ij = 1 if edge i→j exists. - // Power iteration: psiNext_j = alpha * Σ_{i: i→j} psi_i + teleport * Σ_k psi_k - // - // We iterate over source nodes and scatter to targets. - - psiNext.fill(0); - - // Compute sum of psi for teleportation - let psiSum = 0; - for (let i = 0; i < n; i++) psiSum += psi[i]; - - const teleportContrib = teleport * psiSum; - - // Sparse multiply: scatter from sources to targets - for (let i = 0; i < n; i++) { - const neighbors = adj[i]; - const val = alpha * psi[i]; - for (const j of neighbors) { - psiNext[j] += val; - } - } - - // Add teleportation - for (let i = 0; i < n; i++) { - psiNext[i] += teleportContrib; - } - - // Normalize to unit L2 - let norm = 0; - for (let i = 0; i < n; i++) norm += psiNext[i] * psiNext[i]; - norm = Math.sqrt(norm); - if (norm > 0) { - for (let i = 0; i < n; i++) psiNext[i] /= norm; - } - - // Check convergence: L2 norm of difference - let diff = 0; - for (let i = 0; i < n; i++) { - const d = psiNext[i] - psi[i]; - diff += d * d; - } - diff = Math.sqrt(diff); - - // Swap buffers - const tmp = psi; - psi = psiNext; - psiNext = tmp; - - if (diff < tol) { - iter++; - break; - } - } - - // Ensure all components are positive (Perron-Frobenius: dominant eigenvector is non-negative, - // but numerical noise can produce tiny negatives). Clamp to 0. - for (let i = 0; i < n; i++) { - if (psi[i] < 0) psi[i] = 0; - } - - // Write ψ_i into each entity record - for (let i = 0; i < n; i++) { - gf.setPsi(offsets[i], psi[i]); - } - - return iter; -} diff --git a/src/migrate.ts b/src/migrate.ts new file mode 100644 index 0000000..09f996b --- /dev/null +++ b/src/migrate.ts @@ -0,0 +1,280 @@ +/** + * v1/v2 -> v3 migration (logical rebuild), wired to run AUTOMATICALLY on open. + * + * Per Decision_V3Migration_Biscuit: enumerate the old KB by name/strings, + * recreate every entity + relation in a fresh v3 store, and PRESERVE the exact + * mtime/obsMtime/visits/psi/totals. No JSONL; the high-level C ops build the + * structure, then the migration setters restore the fields the create / + * relation ops clobber (addObservation forces mtime=obsMtime; createRelation + * bumps the source mtime). + * + * Per Decision_AutoMigrateOnOpen: {@link autoMigrateToV3} runs in the server's + * open path — detect old format, back the old files up to `.premigrate`, + * rebuild v3 in place, validate, then open. Mirrors the old v1->v2 auto-migrate. + * + * Per Constraint_NoLiveMigration: in-place migration replaces the files, which + * breaks any concurrently-running old instance still mmap'ing them. The deploy + * must stop old instances first. The `.premigrate` backup is kept for recovery. + */ +import path from 'path'; +import { existsSync, openSync, readSync, closeSync, renameSync, rmSync } from 'fs'; +import { GraphFile, DIR_FORWARD } from './graphfile.js'; +import { StringTable } from './stringtable.js'; +import { Store, migrationLock, migrationUnlock } from './store.js'; + +const MEMFILE_MAGIC = 0x4d454d46; // "MEMF" (native MEMFILE_MAGIC) + +interface OldEntity { + name: string; + type: string; + obs: string[]; + mtime: bigint; + obsMtime: bigint; + sv: bigint; + wv: bigint; + psi: number; +} +interface OldRelation { from: string; to: string; relType: string; mtime: bigint; } +interface OldData { entities: OldEntity[]; relations: OldRelation[]; structuralTotal: bigint; walkerTotal: bigint; } +interface PathPair { graph: string; strings: string; } + +export interface MigrateReport { + entities: number; + relations: number; + ok: boolean; + mismatches: string[]; +} + +function derivePaths(base: string): PathPair { + const dir = path.dirname(base); + const b = path.basename(base, path.extname(base)); + return { graph: path.join(dir, `${b}.graph`), strings: path.join(dir, `${b}.strings`) }; +} + +/** + * Classify a `.graph` file by its memfile header (both formats store the u32 + * version at offset 4; v3 also carries the MEMF magic at offset 0). + * 'fresh' = absent / too small to have a header (open will create v3) + * 'v3' = already current + * 'old' = v1/v2 (needs migration) + */ +export function detectGraphFormat(graphPath: string): 'v3' | 'old' | 'fresh' { + if (!existsSync(graphPath)) return 'fresh'; + const fd = openSync(graphPath, 'r'); + try { + const buf = Buffer.alloc(8); + if (readSync(fd, buf, 0, 8, 0) < 8) return 'fresh'; + const magic = buf.readUInt32LE(0); + const version = buf.readUInt32LE(4); + if (magic === MEMFILE_MAGIC && version === 3) return 'v3'; + if (version === 1 || version === 2) return 'old'; + throw new Error(`unrecognized graph format (magic=0x${magic.toString(16)}, version=${version})`); + } finally { + closeSync(fd); + } +} + +/** Read everything out of an old (v1/v2) KB into plain JS. */ +function readOld(p: PathPair): OldData { + const st = new StringTable(p.strings); + const gf = new GraphFile(p.graph, st); // may migrate v1->v2 in place (on the copy/backup) + + const entities: OldEntity[] = []; + const relations: OldRelation[] = []; + let structuralTotal = 0n; + let walkerTotal = 0n; + + gf.lockShared(); + try { + const offsets = gf.getAllEntityOffsets(); + const nameOf = (off: bigint): string => st.get(BigInt(gf.readEntity(off).nameId)); + + for (const off of offsets) { + const r = gf.readEntity(off); + const obs: string[] = []; + if (r.obs0Id !== 0) obs.push(st.get(BigInt(r.obs0Id))); + if (r.obs1Id !== 0) obs.push(st.get(BigInt(r.obs1Id))); + entities.push({ + name: st.get(BigInt(r.nameId)), + type: st.get(BigInt(r.typeId)), + obs, + mtime: r.mtime, + obsMtime: r.obsMtime, + sv: r.structuralVisits, + wv: r.walkerVisits, + psi: r.psi, + }); + } + + for (const off of offsets) { + const r = gf.readEntity(off); + const fromName = st.get(BigInt(r.nameId)); + for (const e of gf.getEdges(off)) { + if (e.direction !== DIR_FORWARD) continue; // forward edges only (each relation once) + relations.push({ + from: fromName, + to: nameOf(e.targetOffset), + relType: st.get(BigInt(e.relTypeId)), + mtime: e.mtime, + }); + } + } + + structuralTotal = gf.getStructuralTotal(); + walkerTotal = gf.getWalkerTotal(); + } finally { + gf.unlock(); + } + gf.close(); + st.close(); + return { entities, relations, structuralTotal, walkerTotal }; +} + +/** Write the old data into a fresh v3 store, preserving every field. */ +function writeV3(p: PathPair, old: OldData): Store { + const store = new Store(p.graph, p.strings); + store.lockExclusive(); + try { + store.refresh(); + for (const e of old.entities) { + const off = store.createEntity(e.name, e.type, e.mtime); + for (const o of e.obs) store.addObservation(off, o, e.obsMtime); + } + for (const r of old.relations) { + const f = store.lookup(r.from); + const t = store.lookup(r.to); + if (f === 0n || t === 0n) continue; // dangling ref in old data; skip + store.createRelation(f, t, r.relType, r.mtime); + } + // Restore preserved fields LAST: addObservation + createRelation clobbered + // mtime/obsMtime and bumped the source mtime. + for (const e of old.entities) { + const off = store.lookup(e.name); + store.setEntityFields(off, e.mtime, e.obsMtime, e.sv, e.wv, e.psi); + } + store.setTotals(old.structuralTotal, old.walkerTotal); + store.sync(); + } finally { + store.unlock(); + } + return store; +} + +/** Validate the v3 store matches the old data field-for-field. */ +function validate(store: Store, old: OldData): string[] { + const mismatches: string[] = []; + store.lockShared(); + try { + store.refresh(); + if (store.entityCount() !== old.entities.length) + mismatches.push(`entityCount ${store.entityCount()} != ${old.entities.length}`); + if (store.relationCount() !== old.relations.length) + mismatches.push(`relationCount ${store.relationCount()} != ${old.relations.length}`); + + for (const e of old.entities) { + const off = store.lookup(e.name); + if (off === 0n) { mismatches.push(`missing entity ${e.name}`); continue; } + const r = store.readEntity(off); + if (r.type !== e.type) mismatches.push(`${e.name}: type ${r.type} != ${e.type}`); + if (r.mtime !== e.mtime) mismatches.push(`${e.name}: mtime ${r.mtime} != ${e.mtime}`); + if (r.obsMtime !== e.obsMtime) mismatches.push(`${e.name}: obsMtime ${r.obsMtime} != ${e.obsMtime}`); + if (r.structuralVisits !== e.sv) mismatches.push(`${e.name}: sv ${r.structuralVisits} != ${e.sv}`); + if (r.walkerVisits !== e.wv) mismatches.push(`${e.name}: wv ${r.walkerVisits} != ${e.wv}`); + if (r.psi !== e.psi) mismatches.push(`${e.name}: psi ${r.psi} != ${e.psi}`); + const want = new Set(e.obs); + if (r.observations.length !== e.obs.length || !r.observations.every(o => want.has(o))) + mismatches.push(`${e.name}: obs [${r.observations}] != [${e.obs}]`); + } + + for (const rel of old.relations) { + const f = store.lookup(rel.from); + if (f === 0n) { mismatches.push(`relation source missing: ${rel.from}`); continue; } + const t = store.lookup(rel.to); + const hit = store.edges(f).some(e => e.direction === 0 && e.target === t && e.relType === rel.relType); + if (!hit) mismatches.push(`missing relation ${rel.from} -${rel.relType}-> ${rel.to}`); + } + + if (store.structuralTotal() !== old.structuralTotal) + mismatches.push(`structuralTotal ${store.structuralTotal()} != ${old.structuralTotal}`); + if (store.walkerTotal() !== old.walkerTotal) + mismatches.push(`walkerTotal ${store.walkerTotal()} != ${old.walkerTotal}`); + } finally { + store.unlock(); + } + return mismatches; +} + +/** Migrate old files (`oldP`, MUST be a copy/backup) into fresh v3 files (`newP`). */ +export function migratePaths(oldP: PathPair, newP: PathPair): MigrateReport { + const old = readOld(oldP); + const store = writeV3(newP, old); + const mismatches = validate(store, old); + store.close(); + return { + entities: old.entities.length, + relations: old.relations.length, + ok: mismatches.length === 0, + mismatches, + }; +} + +/** Migrate by base path (`.graph`/`.strings` derived). `oldBase` MUST be a copy. */ +export function migrate(oldBase: string, newBase: string): MigrateReport { + return migratePaths(derivePaths(oldBase), derivePaths(newBase)); +} + +/** + * Auto-migrate an old (v1/v2) KB to v3 IN PLACE. Backs the old files up to + * `.premigrate`, rebuilds v3 at the original paths, validates, and (on any + * failure) restores the backup before throwing. Caller must have already + * confirmed the format is 'old' (via {@link detectGraphFormat}). + */ +/** + * Core in-place rebuild: rename old files to `.premigrate`, build v3 at the + * original paths, validate, and restore the backup on any failure. The caller + * MUST hold the migration lock and have confirmed the format is 'old'. + */ +function migrateInPlace(graphPath: string, strPath: string): MigrateReport { + const bakGraph = `${graphPath}.premigrate`; + const bakStr = `${strPath}.premigrate`; + renameSync(graphPath, bakGraph); + renameSync(strPath, bakStr); + + const restore = (): void => { + rmSync(graphPath, { force: true }); + rmSync(strPath, { force: true }); + renameSync(bakGraph, graphPath); + renameSync(bakStr, strPath); + }; + + let report: MigrateReport; + try { + report = migratePaths({ graph: bakGraph, strings: bakStr }, { graph: graphPath, strings: strPath }); + } catch (e) { + restore(); + throw e; + } + if (!report.ok) { + restore(); + throw new Error(`v3 auto-migration failed (data preserved in ${bakGraph}/${bakStr}): ${report.mismatches.join('; ')}`); + } + return report; +} + +/** + * Ensure the KB is v3, migrating from v1/v2 in place if needed. Holds an + * exclusive migration flock across BOTH detection and migration, so concurrent + * server startups serialize: the first migrates, the rest wait then observe v3. + * This closes the race where a process reads the file mid-rename, sees it as + * 'fresh', and clobbers the KB with an empty store. Returns the migration + * report if a migration ran, else null (already v3, or a fresh/empty KB). + */ +export function ensureV3(graphPath: string, strPath: string): MigrateReport | null { + const lockFd = migrationLock(`${graphPath}.migrate.lock`); + try { + if (detectGraphFormat(graphPath) !== 'old') return null; // fresh or already v3 + return migrateInPlace(graphPath, strPath); + } finally { + migrationUnlock(lockFd); + } +} diff --git a/src/pagerank.ts b/src/pagerank.ts deleted file mode 100644 index 8d84e0a..0000000 --- a/src/pagerank.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * PageRank sampling — structural rank via random walks on graph topology. - * - * Implements MC complete-path stopping at dangling nodes (Algorithm 4 from - * Avrachenkov et al. "Monte Carlo methods in PageRank computation"). - * - * One "iteration" = one random walk starting from each node in the graph. - * Each walk follows forward edges with probability c (damping factor), - * terminates with probability (1-c) at each step, and stops immediately - * at dangling nodes (nodes with no forward edges). - * - * Every node visited along the walk gets its structuralVisits incremented. - * The global structuralTotal tracks the sum across all visits. - * PageRank(j) = structuralVisits(j) / structuralTotal. - * - * For 14K nodes with c=0.85, one iteration produces ~93K visits total - * (14K starts × ~6.67 avg walk length) and gives <7% error for top pages. - */ - -import { type GraphFile, DIR_FORWARD } from './graphfile.js'; - -const DEFAULT_DAMPING = 0.85; - -/** - * Run one full iteration of structural PageRank sampling. - * Starts one random walk from every node in the graph. - * - * @param gf GraphFile to sample on - * @param damping Probability of following a link (vs terminating). Default 0.85. - * @returns Total number of visits recorded in this iteration. - */ -export function structuralIteration(gf: GraphFile, damping: number = DEFAULT_DAMPING): number { - const offsets = gf.getAllEntityOffsets(); - let totalVisits = 0; - - for (const startOffset of offsets) { - totalVisits += structuralWalk(gf, startOffset, damping); - } - - return totalVisits; -} - -/** - * Run a single structural random walk starting from `startOffset`. - * Follows forward edges, counting every visited node (complete path). - * Stops at dangling nodes or with probability (1-damping) at each step. - * - * @returns Number of visits recorded in this walk. - */ -export function structuralWalk(gf: GraphFile, startOffset: bigint, damping: number = DEFAULT_DAMPING): number { - let current = startOffset; - let visits = 0; - - while (true) { - // Visit current node - gf.incrementStructuralVisit(current); - visits++; - - // Get forward edges only - const edges = gf.getEdges(current); - const forward = edges.filter(e => e.direction === DIR_FORWARD); - - // Dangling node — stop (Algorithm 4) - if (forward.length === 0) break; - - // Terminate with probability (1 - damping) - if (Math.random() >= damping) break; - - // Follow a random forward edge - const idx = Math.floor(Math.random() * forward.length); - current = forward[idx].targetOffset; - } - - return visits; -} - -/** - * Run N iterations of structural sampling. - * Each iteration = one walk from every node. - * - * @returns Total visits across all iterations. - */ -export function structuralSample(gf: GraphFile, iterations: number = 1, damping: number = DEFAULT_DAMPING): number { - let total = 0; - for (let i = 0; i < iterations; i++) { - total += structuralIteration(gf, damping); - } - return total; -} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..8ea930f --- /dev/null +++ b/src/store.ts @@ -0,0 +1,184 @@ +/** + * Store — typed wrapper over the v3 C graph store (graphstore.node). + * + * The C store IS the database: it owns the memfile, the string table, the name + * index, traversal, search, ranking and validation. This class is a thin + * marshaling layer. All entity/relation offsets are BigInt (u64 in C). + */ +import { createRequire } from 'module'; +import { existsSync } from 'fs'; +import { dirname, join, parse as parsePath } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); + +// Edge / traversal direction codes (must match graph.h). +export const DIR_FORWARD = 0; +export const DIR_BACKWARD = 1; +export const DIR_ANY = 255; + +export type Direction = 'forward' | 'backward' | 'any'; +export function dirCode(d: Direction): number { + return d === 'forward' ? DIR_FORWARD : d === 'backward' ? DIR_BACKWARD : DIR_ANY; +} + +/** Resolved entity record as returned by the native readEntity op. */ +export interface NativeEntity { + name: string; + type: string; + observations: string[]; + mtime: bigint; + obsMtime: bigint; + structuralVisits: bigint; + walkerVisits: bigint; + psi: number; +} + +/** One adjacency entry; relType is resolved to its string by the native op. */ +export interface NativeEdge { + target: bigint; + direction: number; // DIR_FORWARD | DIR_BACKWARD + relType: string; + mtime: bigint; +} + +interface NativeStore { + open(graphPath: string, strPath: string, initialSize: number): unknown; + close(h: unknown): void; + sync(h: unknown): void; + lockShared(h: unknown): void; + lockExclusive(h: unknown): void; + unlock(h: unknown): void; + refresh(h: unknown): void; + lookup(h: unknown, name: string): bigint; + createEntity(h: unknown, name: string, type: string, mtime: bigint): bigint; + deleteEntity(h: unknown, offset: bigint): boolean; + readEntity(h: unknown, offset: bigint): NativeEntity; + entityName(h: unknown, offset: bigint): string; + addObservation(h: unknown, offset: bigint, obs: string, mtime: bigint): boolean; + removeObservation(h: unknown, offset: bigint, obs: string, mtime: bigint): boolean; + createRelation(h: unknown, from: bigint, to: bigint, relType: string, mtime: bigint): void; + deleteRelation(h: unknown, from: bigint, to: bigint, relType: string): boolean; + edges(h: unknown, offset: bigint): NativeEdge[]; + neighbors(h: unknown, start: bigint, depth: number, direction: number): bigint[]; + findPath(h: unknown, from: bigint, to: bigint, maxDepth: number, direction: number, budgetBytes: bigint): { path: bigint[]; targetReached: boolean; budgetExhausted: boolean; farthest: bigint }; + search(h: unknown, pattern: string): bigint[]; + entitiesByType(h: unknown, type: string): bigint[]; + orphaned(h: unknown): bigint[]; + listEntities(h: unknown): bigint[]; + entityTypes(h: unknown): string[]; + relationTypes(h: unknown): string[]; + entityCount(h: unknown): number; + relationCount(h: unknown): number; + incWalkerVisit(h: unknown, offset: bigint): void; + incStructuralVisit(h: unknown, offset: bigint): void; + structuralTotal(h: unknown): bigint; + walkerTotal(h: unknown): bigint; + structuralRank(h: unknown, offset: bigint): number; + walkerRank(h: unknown, offset: bigint): number; + getPsi(h: unknown, offset: bigint): number; + structuralSample(h: unknown, iterations: number, damping: number): number; + computeMerwPsi(h: unknown, alpha: number, maxIter: number, tol: number): number; + seedRng(h: unknown, seed: bigint): void; + randomWalk(h: unknown, start: bigint, depth: number, direction: number, merwMode: number, seed: bigint): bigint[]; + validateObs(h: unknown): { offset: bigint; count: number; oversize: number }[]; + validateDangling(h: unknown): { src: bigint; target: bigint }[]; + setEntityFields(h: unknown, off: bigint, mtime: bigint, obsMtime: bigint, structuralVisits: bigint, walkerVisits: bigint, psi: number): void; + setTotals(h: unknown, structuralTotal: bigint, walkerTotal: bigint): void; + lockPath(path: string): number; + unlockPath(fd: number): void; +} + +/** + * Acquire an exclusive kernel flock on a lock file (blocks until held; + * auto-released if this process dies). Used to serialize the one-time v3 + * migration so concurrent server startups can't race it. Returns an fd handle + * (< 0 / huge on failure — caller proceeds best-effort). + */ +export function migrationLock(path: string): number { return native.lockPath(path); } +export function migrationUnlock(fd: number): void { native.unlockPath(fd); } + +// Walk up from __dirname to find build/Release/graphstore.node. Works from +// source (src/), compiled (dist/src/), and npx cache contexts. +function findNative(): string { + let dir = __dirname; + const { root } = parsePath(dir); + while (dir !== root) { + const candidate = join(dir, 'build', 'Release', 'graphstore.node'); + if (existsSync(candidate)) return candidate; + dir = dirname(dir); + } + throw new Error('Could not find native graphstore.node — was the C addon built? Run: node-gyp rebuild'); +} + +const native: NativeStore = require(findNative()); + +export class Store { + private h: unknown; + + constructor(graphPath: string, strPath: string, initialSize: number = 65536) { + this.h = native.open(graphPath, strPath, initialSize); + } + + // lifecycle / concurrency + close(): void { native.close(this.h); } + sync(): void { native.sync(this.h); } + lockShared(): void { native.lockShared(this.h); } + lockExclusive(): void { native.lockExclusive(this.h); } + unlock(): void { native.unlock(this.h); } + refresh(): void { native.refresh(this.h); } + + // entities + lookup(name: string): bigint { return native.lookup(this.h, name); } + createEntity(name: string, type: string, mtime: bigint): bigint { return native.createEntity(this.h, name, type, mtime); } + deleteEntity(offset: bigint): boolean { return native.deleteEntity(this.h, offset); } + readEntity(offset: bigint): NativeEntity { return native.readEntity(this.h, offset); } + entityName(offset: bigint): string { return native.entityName(this.h, offset); } + addObservation(offset: bigint, obs: string, mtime: bigint): boolean { return native.addObservation(this.h, offset, obs, mtime); } + removeObservation(offset: bigint, obs: string, mtime: bigint): boolean { return native.removeObservation(this.h, offset, obs, mtime); } + + // relations + createRelation(from: bigint, to: bigint, relType: string, mtime: bigint): void { native.createRelation(this.h, from, to, relType, mtime); } + deleteRelation(from: bigint, to: bigint, relType: string): boolean { return native.deleteRelation(this.h, from, to, relType); } + edges(offset: bigint): NativeEdge[] { return native.edges(this.h, offset); } + + // traversal / search / scans + neighbors(start: bigint, depth: number, direction: Direction): bigint[] { return native.neighbors(this.h, start, depth, dirCode(direction)); } + findPath(from: bigint, to: bigint, maxDepth: number, direction: Direction, budgetBytes: bigint): { path: bigint[]; targetReached: boolean; budgetExhausted: boolean; farthest: bigint } { + return native.findPath(this.h, from, to, maxDepth, dirCode(direction), budgetBytes); + } + search(pattern: string): bigint[] { return native.search(this.h, pattern); } + entitiesByType(type: string): bigint[] { return native.entitiesByType(this.h, type); } + orphaned(): bigint[] { return native.orphaned(this.h); } + listEntities(): bigint[] { return native.listEntities(this.h); } + entityTypes(): string[] { return native.entityTypes(this.h); } + relationTypes(): string[] { return native.relationTypes(this.h); } + entityCount(): number { return native.entityCount(this.h); } + relationCount(): number { return native.relationCount(this.h); } + + // ranking + incWalkerVisit(offset: bigint): void { native.incWalkerVisit(this.h, offset); } + incStructuralVisit(offset: bigint): void { native.incStructuralVisit(this.h, offset); } + structuralTotal(): bigint { return native.structuralTotal(this.h); } + walkerTotal(): bigint { return native.walkerTotal(this.h); } + structuralRank(offset: bigint): number { return native.structuralRank(this.h, offset); } + walkerRank(offset: bigint): number { return native.walkerRank(this.h, offset); } + getPsi(offset: bigint): number { return native.getPsi(this.h, offset); } + structuralSample(iterations: number, damping: number): number { return native.structuralSample(this.h, iterations, damping); } + computeMerwPsi(alpha: number, maxIter: number, tol: number): number { return native.computeMerwPsi(this.h, alpha, maxIter, tol); } + seedRng(seed: bigint): void { native.seedRng(this.h, seed); } + randomWalk(start: bigint, depth: number, direction: Direction, merwMode: boolean, seed: bigint): bigint[] { + return native.randomWalk(this.h, start, depth, dirCode(direction), merwMode ? 1 : 0, seed); + } + + // validate + validateObs(): { offset: bigint; count: number; oversize: number }[] { return native.validateObs(this.h); } + validateDangling(): { src: bigint; target: bigint }[] { return native.validateDangling(this.h); } + + // migration: restore preserved fields after the logical rebuild. + setEntityFields(off: bigint, mtime: bigint, obsMtime: bigint, structuralVisits: bigint, walkerVisits: bigint, psi: number): void { + native.setEntityFields(this.h, off, mtime, obsMtime, structuralVisits, walkerVisits, psi); + } + setTotals(structuralTotal: bigint, walkerTotal: bigint): void { native.setTotals(this.h, structuralTotal, walkerTotal); } +} diff --git a/tests/migrate.test.ts b/tests/migrate.test.ts new file mode 100644 index 0000000..a33f268 --- /dev/null +++ b/tests/migrate.test.ts @@ -0,0 +1,94 @@ +import { mkdtempSync, rmSync, existsSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { GraphFile, DIR_FORWARD, DIR_BACKWARD } from '../src/graphfile.js'; +import { StringTable } from '../src/stringtable.js'; +import { Store } from '../src/store.js'; +import { migrate, detectGraphFormat } from '../src/migrate.js'; +import { KnowledgeGraphManager } from '../server.js'; + +describe('v3 migrator (v1/v2 -> v3 logical rebuild)', () => { + let dir: string; + beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'mig-')); }); + afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); + + it('rebuilds an old v2 KB into v3, preserving fields exactly', () => { + const now = 1700000000000n; + + // --- synthesize an old-format KB (a copy / scratch, never the live one) --- + const st = new StringTable(join(dir, 'old.strings')); + const gf = new GraphFile(join(dir, 'old.graph'), st); + gf.lockExclusive(); + const alice = gf.createEntity('Alice', 'Person', now, now); + gf.addObservation(alice.offset, 'likes tea', now + 5n); + const bob = gf.createEntity('Bob', 'Person', now + 1n, 0n); // no observations + const acme = gf.createEntity('Acme', 'Org', now + 2n, 0n); + + const addRel = (from: bigint, to: bigint, rt: string, mtime: bigint) => { + const idF = Number(st.intern(rt)); + gf.addEdge(from, { targetOffset: to, direction: DIR_FORWARD, relTypeId: idF, mtime }); + const idB = Number(st.intern(rt)); + gf.addEdge(to, { targetOffset: from, direction: DIR_BACKWARD, relTypeId: idB, mtime }); + }; + addRel(alice.offset, bob.offset, 'KNOWS', now + 10n); + addRel(alice.offset, acme.offset, 'WORKS_AT', now + 11n); + + gf.incrementWalkerVisit(alice.offset); + gf.incrementWalkerVisit(alice.offset); + gf.incrementStructuralVisit(bob.offset); + gf.setPsi(alice.offset, 0.42); + gf.unlock(); + gf.sync(); st.sync(); + gf.close(); st.close(); + + // --- migrate --- + const report = migrate(join(dir, 'old.json'), join(dir, 'new.json')); + expect(report.mismatches).toEqual([]); + expect(report.ok).toBe(true); + expect(report.entities).toBe(3); + expect(report.relations).toBe(2); + + // --- independent spot-check of the v3 result --- + const s = new Store(join(dir, 'new.graph'), join(dir, 'new.strings')); + const a = s.readEntity(s.lookup('Alice')); + expect(a.type).toBe('Person'); + expect(a.observations).toEqual(['likes tea']); + expect(a.walkerVisits).toBe(2n); + expect(a.psi).toBeCloseTo(0.42, 10); + const b = s.readEntity(s.lookup('Bob')); + expect(b.obsMtime).toBe(0n); // no-obs entity keeps obsMtime 0 + expect(b.structuralVisits).toBe(1n); + expect(s.walkerTotal()).toBe(2n); + s.close(); + }); + + it('KnowledgeGraphManager auto-migrates an old KB on open (in place, backup kept)', async () => { + const now = 1700000000000n; + + // old-format KB at the manager's derived paths + const st = new StringTable(join(dir, 'mem.strings')); + const gf = new GraphFile(join(dir, 'mem.graph'), st); + gf.lockExclusive(); + const a = gf.createEntity('Alice', 'Person', now, now); + gf.addObservation(a.offset, 'likes tea', now + 5n); + const bob = gf.createEntity('Bob', 'Person', now + 1n, 0n); + const idF = Number(st.intern('KNOWS')); + gf.addEdge(a.offset, { targetOffset: bob.offset, direction: DIR_FORWARD, relTypeId: idF, mtime: now + 10n }); + const idB = Number(st.intern('KNOWS')); + gf.addEdge(bob.offset, { targetOffset: a.offset, direction: DIR_BACKWARD, relTypeId: idB, mtime: now + 10n }); + gf.unlock(); gf.sync(); st.sync(); gf.close(); st.close(); + + expect(detectGraphFormat(join(dir, 'mem.graph'))).toBe('old'); + + // constructing the manager triggers the in-place auto-migration + const mgr = new KnowledgeGraphManager(join(dir, 'mem.json')); + const graph = await mgr.openNodes(['Alice'], 'forward'); + expect(graph.entities[0]?.name).toBe('Alice'); + expect(graph.entities[0]?.observations).toEqual(['likes tea']); + expect(graph.relations.some(r => r.from === 'Alice' && r.to === 'Bob' && r.relationType === 'KNOWS')).toBe(true); + mgr.close(); + + expect(detectGraphFormat(join(dir, 'mem.graph'))).toBe('v3'); // converted in place + expect(existsSync(join(dir, 'mem.graph.premigrate'))).toBe(true); // backup retained + }); +}); From 17ba411e35b8f3fce25b52a3328cf9155c9cdc76 Mon Sep 17 00:00:00 2001 From: Levalicious <24482560+Levalicious@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:41:52 -0400 Subject: [PATCH 13/17] fix(lint): drop dead find_path budget consts; remove redundant smoke test The find_path byte budget is enforced in C (graph_find_path_ex), so the V8 container-slot constants in server.ts were unused (no-unused-vars). The native/bench/test_binding.cjs smoke test duplicated jest coverage and tripped no-require-imports on a CommonJS file; the jest suite + C harnesses cover the binding. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfzTKfjTRRNQ828crWoJf1 --- native/bench/test_binding.cjs | 48 ----------------------------------- server.ts | 46 +++------------------------------ 2 files changed, 3 insertions(+), 91 deletions(-) delete mode 100644 native/bench/test_binding.cjs diff --git a/native/bench/test_binding.cjs b/native/bench/test_binding.cjs deleted file mode 100644 index bb0bc94..0000000 --- a/native/bench/test_binding.cjs +++ /dev/null @@ -1,48 +0,0 @@ -// Smoke test for the v3 graphstore N-API addon (C<->JS marshaling). -const native = require('../../build/Release/graphstore.node'); -const fs = require('fs'); -const j = (v) => JSON.stringify(v, (k, x) => (typeof x === 'bigint' ? x.toString() : x)); - -for (const f of ['/tmp/gb_g.dat', '/tmp/gb_s.dat']) { try { fs.unlinkSync(f); } catch {} } -const h = native.open('/tmp/gb_g.dat', '/tmp/gb_s.dat', 65536); - -const alice = native.createEntity(h, 'Alice', 'Person', 100n); -const bob = native.createEntity(h, 'Bob', 'Person', 100n); -const acme = native.createEntity(h, 'Acme', 'Org', 100n); -native.createRelation(h, alice, bob, 'KNOWS', 100n); -native.createRelation(h, alice, acme, 'WORKS_AT', 100n); -native.addObservation(h, alice, 'likes tea', 100n); - -let ok = true; -const expect = (cond, msg) => { if (!cond) { ok = false; console.log('FAIL:', msg); } else console.log('ok: ', msg); }; - -expect(native.entityCount(h) === 3, 'entityCount == 3'); -expect(native.relationCount(h) === 2, 'relationCount == 2'); -expect(native.lookup(h, 'Alice') === alice, 'lookup Alice round-trips'); -expect(native.lookup(h, 'Nobody') === 0n, 'lookup miss -> 0n'); - -const arec = native.readEntity(h, alice); -expect(arec.name === 'Alice' && arec.type === 'Person', 'readEntity name/type'); -expect(arec.observations.length === 1 && arec.observations[0] === 'likes tea', 'readEntity observations'); - -const nbrs = native.neighbors(h, alice, 1, 0).map((o) => native.entityName(h, o)).sort(); -expect(j(nbrs) === j(['Acme', 'Bob']), 'neighbors(Alice, fwd, d1) = [Acme, Bob]'); - -const path = native.findPath(h, bob, acme, 4, 255).map((o) => native.entityName(h, o)); -expect(j(path) === j(['Bob', 'Alice', 'Acme']), 'findPath Bob->Acme (any) = Bob,Alice,Acme'); - -expect(native.search(h, 'Person').length === 2, 'search Person -> 2'); -expect(native.entityTypes(h).sort().join(',') === 'Org,Person', 'entityTypes'); -expect(native.relationTypes(h).sort().join(',') === 'KNOWS,WORKS_AT', 'relationTypes'); - -native.structuralSample(h, 1, 0.85); -native.computeMerwPsi(h, 0.85, 200, 1e-8); -expect(native.structuralTotal(h) > 0n, 'structural sampling recorded visits'); -expect(native.getPsi(h, alice) > 0, 'merw psi populated'); -native.incWalkerVisit(h, alice); -expect(native.walkerTotal(h) === 1n && native.walkerRank(h, alice) > 0, 'walker counting works'); - -native.sync(h); -native.close(h); -console.log(ok ? '\nALL PASS' : '\nFAILED'); -process.exit(ok ? 0 : 1); // exits clean: binding.gyp drops -lnode, so no 2nd libnode/libstdc++ static-dtor teardown diff --git a/server.ts b/server.ts index 389a4dd..4d8a288 100644 --- a/server.ts +++ b/server.ts @@ -45,53 +45,13 @@ const HAS_REGEX_META = /[\\^$.*+?()[\]{}|]/; // RAM at call time. Hard upper bound by request: never claim more than 80% // of available memory. // -// We count ONLY what this BFS puts into its own data structures — never -// `process.memoryUsage()` (which conflates with OTel spans, request handler -// state, JIT, anything else allocating in the same isolate). The summed -// cost has two parts, both exact (no per-entry heuristic): -// -// 1. String payload bytes. Every name + relationType we materialize into -// `visited` / `parent` is decoded out of the mmap'd strings file. We -// measure that string with `Buffer.byteLength(s, 'utf-8')` at the -// point of insertion. UTF-8 length is the wire size; V8 internally -// stores Latin-1 (1 B/char) or UCS-2 (2 B/char), and either way the -// payload cost is bounded between 1× and 2× the UTF-8 length. We -// count UTF-8 length and accept the ≤2× ceiling on this term. -// -// 2. V8 container-slot overhead. The Set/Map/Object wrappers V8 puts -// around our payload have fixed per-entry sizes derived from V8's -// OrderedHashSet / OrderedHashMap / JSObject layouts (64-bit, no -// pointer compression). The constants below are structural — they -// came from v8/src/objects/{ordered-hash-table,js-objects}.h, not -// from "let's add a safety factor." If Node's V8 is updated with -// different layouts, re-derive from those headers. -// -// Both terms are summed into `bytesUsed` as we go; when it crosses -// `budgetBytes = 0.80 * MemAvailable`, the BFS terminates and the call -// returns `path=[]` with `kb.traversal.budget_exhausted=true`. -// -// `KB_FIND_PATH_BUDGET_BYTES` env override replaces the live RAM -// calculation — used by tests to force exhaustion in deterministic -// graphs, and by ops to hard-cap on constrained machines. +// The budget VALUE is `0.80 * MemAvailable` (or the `KB_FIND_PATH_BUDGET_BYTES` +// env override, used by tests to force exhaustion). It is passed to the C BFS, +// which enforces it against its own per-node footprint (graph_find_path_ex). // ============================================================================= const FIND_PATH_MEMORY_FRACTION = 0.80; -// V8 container-slot sizes, 64-bit, no pointer compression. Each constant -// is a fixed structural cost V8 imposes on top of the user-payload bytes; -// they are NOT empirical fudge factors. -// -// V8_SET_ENTRY: OrderedHashSet hash-table slot — 1 ptr (key) + 1 ptr -// (chain) + 1 word (hash + tags). 3 × 8 B. -// V8_MAP_ENTRY: OrderedHashMap slot — 1 ptr (key) + 1 ptr (value) -// + 1 ptr (chain) + 1 word (hash + tags). 4 × 8 B. -// V8_OBJECT_HEADER: JSObject header — Map ptr + properties ptr + -// elements ptr + 1 in-object slot for our (small) -// plain-object payloads. 4 × 8 B. -const V8_SET_ENTRY = 24; -const V8_MAP_ENTRY = 32; -const V8_OBJECT_HEADER = 32; - /** * Bytes of RAM the host considers "available" — i.e., free + reclaimable * cache, the right number for "how much can I grow into without forcing From 014b7b861a2b86254bbb0f8112dc3bd501d16c6a Mon Sep 17 00:00:00 2001 From: Levalicious <24482560+Levalicious@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:50:50 -0400 Subject: [PATCH 14/17] fix(ci): import jest globals in migrate.test.ts tsconfig compiles tests/ (include ./**/*.ts) and the prepare hook runs tsc, so the new test needs the explicit @jest/globals import the other tests already use. Ambient globals aren't surfaced under CI's frozen-lockfile install (only local npm @types/jest provided them), so tsc failed with TS2304/TS2593 on describe/it/expect. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfzTKfjTRRNQ828crWoJf1 --- tests/migrate.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/migrate.test.ts b/tests/migrate.test.ts index a33f268..adc6357 100644 --- a/tests/migrate.test.ts +++ b/tests/migrate.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { mkdtempSync, rmSync, existsSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; From b122b0d3007380e7f5775be794e6754eedcf2cc6 Mon Sep 17 00:00:00 2001 From: Levalicious <24482560+Levalicious@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:22:07 -0400 Subject: [PATCH 15/17] fix(migrate): read old v1/v2 format in pure TS; drop old-addon dependency The migrator read the old format via the old memoryfile.node, which CI/deploy never build (binding.gyp only builds graphstore.node, and the old addon's source became the v3 memoryfile.c -- it can't be rebuilt). That broke the CI test job and, worse, the actual auto-migration for users on first v3 start. readOldRaw parses the old .graph/.strings bytes directly (read-only; layout per graphfile.ts), validated byte-for-byte against the old addon on a copy of the live 67k-entity KB (0 mismatches, totals equal). The migrate test now migrates a committed v2 fixture, so it needs no native old addon either. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfzTKfjTRRNQ828crWoJf1 --- src/migrate.ts | 114 ++++++++++++++++++++-------------- tests/fixtures/old-v2.graph | Bin 0 -> 8192 bytes tests/fixtures/old-v2.strings | Bin 0 -> 36936 bytes tests/migrate.test.ts | 70 ++++++--------------- 4 files changed, 86 insertions(+), 98 deletions(-) create mode 100644 tests/fixtures/old-v2.graph create mode 100644 tests/fixtures/old-v2.strings diff --git a/src/migrate.ts b/src/migrate.ts index 09f996b..6a56ed6 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -17,9 +17,7 @@ * must stop old instances first. The `.premigrate` backup is kept for recovery. */ import path from 'path'; -import { existsSync, openSync, readSync, closeSync, renameSync, rmSync } from 'fs'; -import { GraphFile, DIR_FORWARD } from './graphfile.js'; -import { StringTable } from './stringtable.js'; +import { existsSync, openSync, readSync, closeSync, renameSync, rmSync, readFileSync } from 'fs'; import { Store, migrationLock, migrationUnlock } from './store.js'; const MEMFILE_MAGIC = 0x4d454d46; // "MEMF" (native MEMFILE_MAGIC) @@ -74,59 +72,81 @@ export function detectGraphFormat(graphPath: string): 'v3' | 'old' | 'fresh' { } } -/** Read everything out of an old (v1/v2) KB into plain JS. */ -function readOld(p: PathPair): OldData { - const st = new StringTable(p.strings); - const gf = new GraphFile(p.graph, st); // may migrate v1->v2 in place (on the copy/backup) +/** + * Pure-TS reader for the old v1/v2 on-disk format — read-only, no native addon. + * The byte layout is fixed (see src/graphfile.ts / src/stringtable.ts): + * graph header @40: u64 node_log_off, u64 structural_total, u64 walker_total + * node log: u32 count, u32 cap, u64 offsets[count] + * entity record: nameId@0 typeId@4 adjOff@8 mtime@16 obsMtime@24 obsCount@32 + * obs0@36 obs1@40 sVisits@48 wVisits@56 psi@64 (v2 only; v1=64B) + * adj entry (24B): (target<<2|dir)@0, relType@8, mtime@16 + * string entry @id: u32 refcount, u32 hash, u16 len, u8 data[len] + * This is why the migrator no longer needs the old addon (which can't be rebuilt + * anyway — its source became the v3 memoryfile.c). + */ +export function readOldRaw(p: PathPair): OldData { + const graph = readFileSync(p.graph); + const strings = readFileSync(p.strings); - const entities: OldEntity[] = []; - const relations: OldRelation[] = []; - let structuralTotal = 0n; - let walkerTotal = 0n; + const version = graph.readUInt32LE(4); // memfile header version field + if (version !== 1 && version !== 2) { + throw new Error(`migrate: ${p.graph} is not an old v1/v2 graph (version ${version})`); + } + const hasPsi = version === 2; // psi field only exists in v2 - gf.lockShared(); - try { - const offsets = gf.getAllEntityOffsets(); - const nameOf = (off: bigint): string => st.get(BigInt(gf.readEntity(off).nameId)); + const getStr = (id: number): string => { + if (id === 0) return ''; + const len = strings.readUInt16LE(id + 8); + return strings.toString('utf-8', id + 10, id + 10 + len); + }; + const nameAt = (off: number): string => getStr(graph.readUInt32LE(off)); - for (const off of offsets) { - const r = gf.readEntity(off); - const obs: string[] = []; - if (r.obs0Id !== 0) obs.push(st.get(BigInt(r.obs0Id))); - if (r.obs1Id !== 0) obs.push(st.get(BigInt(r.obs1Id))); - entities.push({ - name: st.get(BigInt(r.nameId)), - type: st.get(BigInt(r.typeId)), - obs, - mtime: r.mtime, - obsMtime: r.obsMtime, - sv: r.structuralVisits, - wv: r.walkerVisits, - psi: r.psi, - }); - } + const GH = 40; + const nodeLogOff = Number(graph.readBigUInt64LE(GH)); + const structuralTotal = graph.readBigUInt64LE(GH + 8); + const walkerTotal = graph.readBigUInt64LE(GH + 16); - for (const off of offsets) { - const r = gf.readEntity(off); - const fromName = st.get(BigInt(r.nameId)); - for (const e of gf.getEdges(off)) { - if (e.direction !== DIR_FORWARD) continue; // forward edges only (each relation once) + const count = graph.readUInt32LE(nodeLogOff); + const offsets: number[] = []; + for (let i = 0; i < count; i++) offsets.push(Number(graph.readBigUInt64LE(nodeLogOff + 8 + i * 8))); + + const entities: OldEntity[] = []; + const relations: OldRelation[] = []; + for (const off of offsets) { + const o0 = graph.readUInt32LE(off + 36); + const o1 = graph.readUInt32LE(off + 40); + const obs: string[] = []; + if (o0 !== 0) obs.push(getStr(o0)); + if (o1 !== 0) obs.push(getStr(o1)); + entities.push({ + name: getStr(graph.readUInt32LE(off)), + type: getStr(graph.readUInt32LE(off + 4)), + obs, + mtime: graph.readBigUInt64LE(off + 16), + obsMtime: graph.readBigUInt64LE(off + 24), + sv: graph.readBigUInt64LE(off + 48), + wv: graph.readBigUInt64LE(off + 56), + psi: hasPsi ? graph.readDoubleLE(off + 64) : 0, + }); + + const adjOff = Number(graph.readBigUInt64LE(off + 8)); + if (adjOff !== 0) { + const fromName = getStr(graph.readUInt32LE(off)); + const aCount = graph.readUInt32LE(adjOff); + for (let i = 0; i < aCount; i++) { + const base = adjOff + 8 + i * 24; + const packed = graph.readBigUInt64LE(base); + if (Number(packed & 3n) !== 0) continue; // forward edges only (DIR_FORWARD = 0) relations.push({ from: fromName, - to: nameOf(e.targetOffset), - relType: st.get(BigInt(e.relTypeId)), - mtime: e.mtime, + to: nameAt(Number(packed >> 2n)), + relType: getStr(graph.readUInt32LE(base + 8)), + mtime: graph.readBigUInt64LE(base + 16), }); } } - - structuralTotal = gf.getStructuralTotal(); - walkerTotal = gf.getWalkerTotal(); - } finally { - gf.unlock(); } - gf.close(); - st.close(); + return { entities, relations, structuralTotal, walkerTotal }; } @@ -206,7 +226,7 @@ function validate(store: Store, old: OldData): string[] { /** Migrate old files (`oldP`, MUST be a copy/backup) into fresh v3 files (`newP`). */ export function migratePaths(oldP: PathPair, newP: PathPair): MigrateReport { - const old = readOld(oldP); + const old = readOldRaw(oldP); const store = writeV3(newP, old); const mismatches = validate(store, old); store.close(); diff --git a/tests/fixtures/old-v2.graph b/tests/fixtures/old-v2.graph new file mode 100644 index 0000000000000000000000000000000000000000..b68f3be381f3c2547cb4e7c97454aabe823ce5c9 GIT binary patch literal 8192 zcmeH}EfT^o5QVq=%s?>+4p6{w3lvb(nMy3K5{(KP!x1V`=-vV}iV9{p0+J28Ya&3@ zF)uU8OE$^dKER( zwbd!MHZ{M>5nZ{@wBBj`@U#1&ZZ+WF-QiwNB=B^ivHISpvZlT0pkY4$Kc88s?96$gBKmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY{35qJYd9xurN literal 0 HcmV?d00001 diff --git a/tests/fixtures/old-v2.strings b/tests/fixtures/old-v2.strings new file mode 100644 index 0000000000000000000000000000000000000000..ded118307934186cc507caff242cf7a530693fa4 GIT binary patch literal 36936 zcmeI&y)Fb%7zW_uH>i}W+g74giNs2XL|E|?mB^9_;xEAk9EE6I0-ZacQAsokT7_P+ zSD+wfnN5~x6q4D^GsXGNd~@c^d!BkGXC`LaLI_i5>5yK9O@IIa0t5&U zAV7cs0RjXF5FpUp0xz+-^Gi>F009C72oNAZ;6DU5B;j@S19dlttX-0 zZ%_N@)xF#9Fqa%0?(Y>F)>oc~pL)a2)^>7OJW48`PwMa0_B*<`uXcp-{k3%7*nhYg zyz305_1)xq|Mm3py(5$lHX7E~zCY6UHrf@YXUoeAp_ty+u3G Bob(Person), -WORKS_AT-> Acme(Org); Bob structuralVisits=1. +const FIXTURE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'fixtures'); +function placeOldV2(graphPath: string, strPath: string): void { + copyFileSync(join(FIXTURE_DIR, 'old-v2.graph'), graphPath); + copyFileSync(join(FIXTURE_DIR, 'old-v2.strings'), strPath); +} + describe('v3 migrator (v1/v2 -> v3 logical rebuild)', () => { let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'mig-')); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); it('rebuilds an old v2 KB into v3, preserving fields exactly', () => { - const now = 1700000000000n; - - // --- synthesize an old-format KB (a copy / scratch, never the live one) --- - const st = new StringTable(join(dir, 'old.strings')); - const gf = new GraphFile(join(dir, 'old.graph'), st); - gf.lockExclusive(); - const alice = gf.createEntity('Alice', 'Person', now, now); - gf.addObservation(alice.offset, 'likes tea', now + 5n); - const bob = gf.createEntity('Bob', 'Person', now + 1n, 0n); // no observations - const acme = gf.createEntity('Acme', 'Org', now + 2n, 0n); - - const addRel = (from: bigint, to: bigint, rt: string, mtime: bigint) => { - const idF = Number(st.intern(rt)); - gf.addEdge(from, { targetOffset: to, direction: DIR_FORWARD, relTypeId: idF, mtime }); - const idB = Number(st.intern(rt)); - gf.addEdge(to, { targetOffset: from, direction: DIR_BACKWARD, relTypeId: idB, mtime }); - }; - addRel(alice.offset, bob.offset, 'KNOWS', now + 10n); - addRel(alice.offset, acme.offset, 'WORKS_AT', now + 11n); + placeOldV2(join(dir, 'old.graph'), join(dir, 'old.strings')); - gf.incrementWalkerVisit(alice.offset); - gf.incrementWalkerVisit(alice.offset); - gf.incrementStructuralVisit(bob.offset); - gf.setPsi(alice.offset, 0.42); - gf.unlock(); - gf.sync(); st.sync(); - gf.close(); st.close(); - - // --- migrate --- const report = migrate(join(dir, 'old.json'), join(dir, 'new.json')); expect(report.mismatches).toEqual([]); expect(report.ok).toBe(true); expect(report.entities).toBe(3); expect(report.relations).toBe(2); - // --- independent spot-check of the v3 result --- const s = new Store(join(dir, 'new.graph'), join(dir, 'new.strings')); const a = s.readEntity(s.lookup('Alice')); expect(a.type).toBe('Person'); @@ -57,28 +39,14 @@ describe('v3 migrator (v1/v2 -> v3 logical rebuild)', () => { expect(a.walkerVisits).toBe(2n); expect(a.psi).toBeCloseTo(0.42, 10); const b = s.readEntity(s.lookup('Bob')); - expect(b.obsMtime).toBe(0n); // no-obs entity keeps obsMtime 0 + expect(b.obsMtime).toBe(0n); // no-obs entity keeps obsMtime 0 expect(b.structuralVisits).toBe(1n); expect(s.walkerTotal()).toBe(2n); s.close(); }); it('KnowledgeGraphManager auto-migrates an old KB on open (in place, backup kept)', async () => { - const now = 1700000000000n; - - // old-format KB at the manager's derived paths - const st = new StringTable(join(dir, 'mem.strings')); - const gf = new GraphFile(join(dir, 'mem.graph'), st); - gf.lockExclusive(); - const a = gf.createEntity('Alice', 'Person', now, now); - gf.addObservation(a.offset, 'likes tea', now + 5n); - const bob = gf.createEntity('Bob', 'Person', now + 1n, 0n); - const idF = Number(st.intern('KNOWS')); - gf.addEdge(a.offset, { targetOffset: bob.offset, direction: DIR_FORWARD, relTypeId: idF, mtime: now + 10n }); - const idB = Number(st.intern('KNOWS')); - gf.addEdge(bob.offset, { targetOffset: a.offset, direction: DIR_BACKWARD, relTypeId: idB, mtime: now + 10n }); - gf.unlock(); gf.sync(); st.sync(); gf.close(); st.close(); - + placeOldV2(join(dir, 'mem.graph'), join(dir, 'mem.strings')); expect(detectGraphFormat(join(dir, 'mem.graph'))).toBe('old'); // constructing the manager triggers the in-place auto-migration @@ -89,7 +57,7 @@ describe('v3 migrator (v1/v2 -> v3 logical rebuild)', () => { expect(graph.relations.some(r => r.from === 'Alice' && r.to === 'Bob' && r.relationType === 'KNOWS')).toBe(true); mgr.close(); - expect(detectGraphFormat(join(dir, 'mem.graph'))).toBe('v3'); // converted in place - expect(existsSync(join(dir, 'mem.graph.premigrate'))).toBe(true); // backup retained + expect(detectGraphFormat(join(dir, 'mem.graph'))).toBe('v3'); // converted in place + expect(existsSync(join(dir, 'mem.graph.premigrate'))).toBe(true); // backup retained }); }); From 3ac2c1e5c97238375662c95c84def79794113528 Mon Sep 17 00:00:00 2001 From: Levalicious <24482560+Levalicious@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:01:05 -0400 Subject: [PATCH 16/17] bench: per-op adaptive microbench + base-vs-head CI gate + creep history Per-op rdtsc microbenchmark for the v3 graph store: - Adaptive sampling (riff RE=CV/sqrt(n) criterion, 2% target, batch-means), resolved in-run rather than persisted; auto-sizes iterations per op (~0.1s/run). - Frequency-reference op so comparisons cancel cross-run CPU-turbo drift; median is the headline metric. CI tooling, no committed baselines (nothing for git to churn): - bench-compare: base-vs-head on one runner, freq-corrected median, median of N interleaved runs, soft-fail + PR comment (benchmark.yml). - bench-aggregate + bench-creep: reference-normalized time-series on an orphan bench-history branch + slow-creep detector vs rolling baseline (benchmark-history.yml, on push to master). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfzTKfjTRRNQ828crWoJf1 --- .github/workflows/benchmark-history.yml | 88 +++++++++++ .github/workflows/benchmark.yml | 88 +++++++++++ native/Makefile | 8 +- native/op_bench.c | 197 ++++++++++++++++++++++++ scripts/bench-aggregate.mjs | 41 +++++ scripts/bench-compare.mjs | 83 ++++++++++ scripts/bench-creep.mjs | 89 +++++++++++ 7 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/benchmark-history.yml create mode 100644 .github/workflows/benchmark.yml create mode 100644 native/op_bench.c create mode 100644 scripts/bench-aggregate.mjs create mode 100644 scripts/bench-compare.mjs create mode 100644 scripts/bench-creep.mjs diff --git a/.github/workflows/benchmark-history.yml b/.github/workflows/benchmark-history.yml new file mode 100644 index 0000000..e9f0bab --- /dev/null +++ b/.github/workflows/benchmark-history.yml @@ -0,0 +1,88 @@ +name: benchmark-history + +# Accumulate a reference-normalized benchmark time-series so slow creep (which the +# per-PR head-vs-base gate is blind to) becomes visible. On every push to master +# we run the bench, append one sample to an append-only NDJSON on the ORPHAN +# `bench-history` branch (isolated from master/PR history — no churn), and check +# the new sample against a rolling baseline. Creep is a SOFT-fail + commit comment. +on: + push: + branches: [master] + +permissions: + contents: write + +concurrency: + group: bench-history + cancel-in-progress: false + +jobs: + accumulate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Build and run bench (interleaved) + run: | + make -C native bench >/dev/null 2>&1 + cp /tmp/mf_test_bench /tmp/bench + for i in $(seq 1 7); do /tmp/bench > "/tmp/run_$i.json"; done + + - name: Aggregate one normalized sample + run: | + R=$(ls /tmp/run_*.json | paste -sd,) + node scripts/bench-aggregate.mjs --runs "$R" \ + --sha "$GITHUB_SHA" --date "$(date -u +%FT%TZ)" > /tmp/sample.ndjson + cat /tmp/sample.ndjson + + - name: Append to bench-history + creep check + id: creep + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git fetch origin bench-history || true + if git rev-parse --verify origin/bench-history >/dev/null 2>&1; then + git worktree add /tmp/hist origin/bench-history + else + git worktree add --detach /tmp/hist + git -C /tmp/hist checkout --orphan bench-history + git -C /tmp/hist rm -rf . >/dev/null 2>&1 || true + fi + touch /tmp/hist/history.ndjson + # creep check: new sample vs the EXISTING history (before appending) + set +e + node scripts/bench-creep.mjs --history /tmp/hist/history.ndjson --sample /tmp/sample.ndjson > /tmp/creep.md + echo "rc=$?" >> "$GITHUB_OUTPUT" + set -e + cat /tmp/creep.md + # append + push (one retry if the branch advanced under us) + cat /tmp/sample.ndjson >> /tmp/hist/history.ndjson + git -C /tmp/hist add history.ndjson + git -C /tmp/hist commit -m "bench: sample for ${GITHUB_SHA}" + git -C /tmp/hist push origin HEAD:bench-history || { + git -C /tmp/hist fetch origin bench-history + git -C /tmp/hist reset --soft origin/bench-history + cat /tmp/sample.ndjson >> /tmp/hist/history.ndjson + git -C /tmp/hist add history.ndjson + git -C /tmp/hist commit -m "bench: sample for ${GITHUB_SHA} (retry)" + git -C /tmp/hist push origin HEAD:bench-history + } + + - name: Comment creep on commit + if: steps.creep.outputs.rc != '0' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const body = '\n' + fs.readFileSync('/tmp/creep.md', 'utf8'); + await github.rest.repos.createCommitComment({ ...context.repo, commit_sha: context.sha, body }); + + - name: Soft-fail on creep + if: steps.creep.outputs.rc != '0' + run: | + echo "::warning::Benchmark slow-creep detected vs rolling baseline — see the commit comment. (soft-fail)" + exit 1 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..8182312 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,88 @@ +name: benchmark + +# Per-op graph-store microbenchmark, base vs head, on ONE runner — no committed +# baseline values (nothing for git to churn). Builds the SAME harness against the +# base and head store sources, runs them interleaved, frequency-corrects head to +# base via a fixed-work reference op, and diffs the medians. SOFT-fail: a +# regression turns the check red and posts a PR comment, but this check is not +# meant to be a required status, so it never hard-blocks a merge. +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + op-bench: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Build head benchmark + run: | + make -C native bench >/dev/null 2>&1 + cp /tmp/mf_test_bench /tmp/bench_head + + - name: Build base benchmark (head harness, base store sources) + id: base + run: | + BASE_SHA="${{ github.event.pull_request.base.sha }}" + git worktree add --detach /tmp/base "$BASE_SHA" + mkdir -p /tmp/base/native + # Overlay the head harness so ONLY the measured store code differs. + cp native/op_bench.c /tmp/base/native/op_bench.c + cp native/Makefile /tmp/base/native/Makefile + if make -C /tmp/base/native bench >/tmp/base_build.log 2>&1; then + cp /tmp/mf_test_bench /tmp/bench_base + echo "ok=1" >> "$GITHUB_OUTPUT" + else + echo "ok=0" >> "$GITHUB_OUTPUT" + echo "Base build failed — base likely has no comparable benchmark/store. Skipping comparison." + tail -25 /tmp/base_build.log || true + fi + + - name: Run interleaved and compare + if: steps.base.outputs.ok == '1' + id: cmp + run: | + for i in $(seq 1 5); do + /tmp/bench_head > "/tmp/head_$i.json" + /tmp/bench_base > "/tmp/base_$i.json" + done + H=$(ls /tmp/head_*.json | paste -sd,) + B=$(ls /tmp/base_*.json | paste -sd,) + set +e + node scripts/bench-compare.mjs --base "$B" --head "$H" > /tmp/report.md + echo "rc=$?" >> "$GITHUB_OUTPUT" + set -e + cat /tmp/report.md + + - name: Comment results on PR + if: steps.base.outputs.ok == '1' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const body = '\n' + fs.readFileSync('/tmp/report.md', 'utf8'); + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }); + const existing = comments.find((c) => c.body && c.body.includes('')); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + + - name: Soft-fail on regression + if: steps.base.outputs.ok == '1' && steps.cmp.outputs.rc != '0' + run: | + echo "::warning::Per-op benchmark regression detected — see the op-bench PR comment. (soft-fail: not a required check)" + exit 1 diff --git a/native/Makefile b/native/Makefile index 85c870d..51612a4 100644 --- a/native/Makefile +++ b/native/Makefile @@ -11,7 +11,7 @@ CFLAGS = -std=c11 -g -O1 -Wall -Wextra -fsanitize=address,undefined \ LIBS = -lm OUT = /tmp/mf_test -.PHONY: test verify-detector test_memfile test_stringtable test_graph test_entity clean +.PHONY: test verify-detector test_memfile test_stringtable test_graph test_entity bench clean # `make test` = prove the detector fires, then run every harness with it active. test: verify-detector test_memfile test_stringtable test_graph test_entity @@ -34,5 +34,11 @@ test_graph: test_graph.c graph.c stringtable.c memoryfile.c test_entity: test_entity.c $(CC) $(CFLAGS) $^ $(LIBS) -o $(OUT)_entity && $(OUT)_entity +# Per-op graph benchmark: optimized build (NO ASan / NO double-free-check — those +# skew timing). Emits per-op rdtsc cycle stats as JSON; CI compares base vs head. +BENCH_CFLAGS = -std=c11 -O2 -march=native -Wall -D_GNU_SOURCE -I. +bench: op_bench.c graph.c stringtable.c memoryfile.c + $(CC) $(BENCH_CFLAGS) $^ -lm -o $(OUT)_bench && $(OUT)_bench + clean: rm -f $(OUT)_* diff --git a/native/op_bench.c b/native/op_bench.c new file mode 100644 index 0000000..db48a39 --- /dev/null +++ b/native/op_bench.c @@ -0,0 +1,197 @@ +/* + * Per-op microbench for the v3 graph store. rdtsc cycles/op over a fixed, seeded + * graph; emits a per-op distribution as JSON on stdout for base-vs-head CI + * regression comparison (no committed baselines — see scripts/bench-compare). + * + * make bench # -O2 -march=native, NO ASan / NO double-free-check + * /tmp/mf_test_bench [N] [seed] + * + * Adaptive sampling (ported from riff/benches/lcs_bench.rs, governed by + * Heuristic_MicrobenchAreStatistical): instead of a hand-tuned fixed iteration + * count per op, each op is sampled until the relative standard error of the mean + * RE = CV/sqrt(n) (CV = stddev/mean) drops below TARGET_RE, or a sample/budget cap + * is hit. Each sample is a BATCH-MEAN (time over B iterations / B): single-op + * times have fat tails (scheduler/cache outliers) that make CV explode, but the + * CV of batch-means is well-behaved, which is what makes the RE criterion sound. + * riff persists per-op counts across runs because Criterion can't grow sample_size + * mid-run; this harness can, so it resolves the target error in-run (stateless). + * + * Reported per op: min/p50/p90/p99/mean (cycles/op) + achieved n + re. p50 (median) + * is the headline comparison metric: robust to tails and representative of typical + * ("average") cost; min/p90/p99 give the floor and tail for context. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include "graph.h" + +static inline u64 tsc_begin(void) { + unsigned a, d; __asm__ __volatile__("lfence\n\trdtsc" : "=a"(a), "=d"(d)); return ((u64)d << 32) | a; +} +static inline u64 tsc_end(void) { + unsigned a, d; __asm__ __volatile__("rdtscp" : "=a"(a), "=d"(d) :: "ecx"); + __asm__ __volatile__("lfence" ::: "memory"); return ((u64)d << 32) | a; +} + +static u64 rng; +static inline u64 xs(void) { u64 x = rng; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return rng = x; } + +/* Frequency reference: fixed compute+memory work. Its cycles track the run's CPU + * frequency (invariant TSC counts wall-clock), so dividing op cost by the + * reference cancels the GLOBAL frequency/turbo factor that otherwise makes the + * same code look 5-13% different run-to-run. The compare script does the math. */ +static u32 g_refbuf[4096]; +static u64 reference_work(void) { + u64 acc = 0; + for (int j = 0; j < 1024; j++) { u64 r = xs(); acc += g_refbuf[r & 4095]; g_refbuf[r & 4095] = (u32)(acc ^ r); } + return acc; +} + +/* Adaptive-sampling tunables (mirror riff's TARGET_RE_UPPER / clamps). */ +#define POOL 4096u +#define POOL_MASK (POOL - 1u) +static const double TARGET_RE = 0.02; /* stop when RE = CV/sqrt(n) <= 2% */ +static const double TARGET_BATCH = 20000.0; /* aim each batch ~20k cycles to amortize rdtsc */ +static const size_t MIN_SAMPLES = 30; +static const size_t MAX_SAMPLES = 2000; +static const u64 BUDGET_CYC = 80000000ull; /* per-op wall cap (~30ms @2.6GHz) for O(N) ops */ + +static int cmp_d(const void *a, const void *b) { double x = *(const double *)a, y = *(const double *)b; return (x > y) - (x < y); } + +static int g_first = 1; +/* Compute distribution + RE from the collected batch-mean samples and emit JSON. */ +static void emit_op(const char *name, double *s, size_t n) { + double mean = 0; for (size_t i = 0; i < n; i++) mean += s[i]; mean /= (double)n; + double var = 0; for (size_t i = 0; i < n; i++) { double d = s[i] - mean; var += d * d; } + var /= (n > 1) ? (double)(n - 1) : 1.0; + double cv = (mean > 0.0) ? sqrt(var) / mean : 0.0; + double re = cv / sqrt((double)n); + qsort(s, n, sizeof(double), cmp_d); + size_t p90 = (size_t)(n * 0.90); if (p90 >= n) p90 = n - 1; + size_t p99 = (size_t)(n * 0.99); if (p99 >= n) p99 = n - 1; + printf("%s \"%s\": {\"min\": %.1f, \"p50\": %.1f, \"p90\": %.1f, \"p99\": %.1f, \"mean\": %.1f, \"n\": %zu, \"re\": %.4f}", + g_first ? "" : ",\n", name, s[0], s[n / 2], s[p90], s[p99], mean, n, re); + g_first = 0; +} + +/* Sample __VA_ARGS__ (a statement using `_k` to index a precomputed input pool) + * in auto-sized batches until RE <= TARGET_RE, or n / cycle budget is exhausted. */ +#define ADAPT(NAME, ...) do { \ + u64 _wmin = ~0ull, _wtot = 0; int _wi = 0; \ + while (_wi < 256 && _wtot < 2000000ull) { size_t _k = (size_t)_wi & POOL_MASK; (void)_k; \ + u64 _a = tsc_begin(); __VA_ARGS__; u64 _b = tsc_end(); u64 _e = _b - _a; \ + _wtot += _e; if (_e < _wmin) _wmin = _e; _wi++; } \ + double _c = (double)_wmin; if (_c < 1.0) _c = 1.0; \ + size_t _B = (size_t)(TARGET_BATCH / _c); if (_B < 1) _B = 1; if (_B > POOL) _B = POOL; \ + double _mean = 0, _m2 = 0, _re = 1.0; size_t _n = 0; u64 _tot = 0; \ + while (_n < MAX_SAMPLES) { \ + u64 _t0 = tsc_begin(); \ + for (size_t _i = 0; _i < _B; _i++) { size_t _k = (_n * _B + _i) & POOL_MASK; (void)_k; __VA_ARGS__; } \ + u64 _t1 = tsc_end(); _tot += (_t1 - _t0); \ + double _s = (double)(_t1 - _t0) / (double)_B; samp[_n] = _s; \ + double _d = _s - _mean; _mean += _d / (double)(_n + 1); _m2 += _d * (_s - _mean); _n++; \ + if (_n >= MIN_SAMPLES) { \ + double _var = _m2 / (double)(_n - 1); \ + double _cv = (_mean > 0.0) ? sqrt(_var) / _mean : 0.0; \ + _re = _cv / sqrt((double)_n); \ + if (_re <= TARGET_RE || _tot >= BUDGET_CYC) break; \ + } \ + } \ + emit_op(NAME, samp, _n); \ +} while (0) + +int main(int argc, char **argv) { + cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set); sched_setaffinity(0, sizeof set, &set); + size_t N = (argc > 1) ? strtoul(argv[1], NULL, 10) : 2000; + rng = (argc > 2) ? strtoull(argv[2], NULL, 10) : 0x9e3779b97f4a7c15ull; + + const char *gp = "/tmp/opbench.graph", *sp = "/tmp/opbench.strings"; + unlink(gp); unlink(sp); + stringtable_t *st = st_open(sp, 1u << 20); + graph_t *g = graph_open(gp, st, 1u << 20); + + u64 *off = malloc(N * sizeof(u64)); + char nm[32], ty[32], ob[64]; + const u64 now = 1700000000000ull; + + for (size_t i = 0; i < N; i++) { + int nl = snprintf(nm, sizeof nm, "ent-%zu", i); + int tl = snprintf(ty, sizeof ty, "type-%zu", i % 20); + off[i] = graph_create_entity(g, (const u8 *)nm, (u16)nl, (const u8 *)ty, (u16)tl, now); + int ol = snprintf(ob, sizeof ob, "obs-a-%zu", i); graph_add_observation(g, off[i], (const u8 *)ob, (u16)ol, now); + ol = snprintf(ob, sizeof ob, "obs-b-%zu", i); graph_add_observation(g, off[i], (const u8 *)ob, (u16)ol, now); + } + for (size_t i = 0; i < N; i++) { + for (int k = 0; k < 3; k++) { + size_t j = xs() % N; if (j == i) continue; + int rl = snprintf(nm, sizeof nm, "rel-%llu", (unsigned long long)(xs() % 8)); + graph_create_relation(g, off[i], off[j], (const u8 *)nm, (u16)rl, now); + } + } + + /* Precompute input pools so the timed regions contain ONLY the op (no per-iter + * snprintf/rng inside timing), which lets us batch cleanly. */ + size_t *idx = malloc(POOL * sizeof *idx); + size_t *idx2 = malloc(POOL * sizeof *idx2); + size_t *tpix = malloc(POOL * sizeof *tpix); + u64 *seed = malloc(POOL * sizeof *seed); + char **pat = malloc(POOL * sizeof *pat); + for (u32 k = 0; k < POOL; k++) { + idx[k] = xs() % N; idx2[k] = xs() % N; tpix[k] = xs() % 20; seed[k] = xs(); + char *p = malloc(32); snprintf(p, 32, "ent-%llu$", (unsigned long long)(xs() % N)); pat[k] = p; + } + char **names = malloc(N * sizeof *names); u16 *nlen = malloc(N * sizeof *nlen); + for (size_t i = 0; i < N; i++) { char *p = malloc(24); int l = snprintf(p, 24, "ent-%zu", i); names[i] = p; nlen[i] = (u16)l; } + char *tynames[20]; u16 tylen[20]; + for (int t = 0; t < 20; t++) { char *p = malloc(16); int l = snprintf(p, 16, "type-%d", t); tynames[t] = p; tylen[t] = (u16)l; } + + printf("{\n \"graph\": {\"entities\": %zu, \"relations\": %u},\n \"ops\": {\n", N, graph_relation_count(g)); + + double *samp = malloc((MAX_SAMPLES > 2048 ? MAX_SAMPLES : 2048) * sizeof *samp); + u32 cap = (u32)N + 4; u64 *out = malloc((size_t)cap * 8); + entity_t e; + volatile u64 sink = 0; volatile u32 sink32 = 0; (void)sink; (void)sink32; + + ADAPT("reference", sink = reference_work()); + ADAPT("lookup", sink = graph_lookup(g, (const u8 *)names[idx[_k]], nlen[idx[_k]])); + ADAPT("read_entity", graph_read_entity(g, off[idx[_k]], &e)); + ADAPT("neighbors_d1", sink32 = graph_neighbors(g, off[idx[_k]], 1, DIR_ANY, out, cap)); + ADAPT("neighbors_d2", sink32 = graph_neighbors(g, off[idx[_k]], 2, DIR_ANY, out, cap)); + ADAPT("find_path_d6", sink32 = graph_find_path(g, off[idx[_k]], off[idx2[_k]], 6, DIR_ANY, out, cap)); + ADAPT("search", sink32 = graph_search(g, pat[_k], out, cap)); + ADAPT("entities_by_type", sink32 = graph_entities_by_type(g, (const u8 *)tynames[tpix[_k]], tylen[tpix[_k]], out, cap)); + ADAPT("random_walk_d5", sink32 = graph_random_walk(g, off[idx[_k]], 5, DIR_ANY, 1, seed[_k], out, 8)); + ADAPT("inc_walker_visit", graph_inc_walker_visit(g, off[idx[_k]])); + ADAPT("structural_sample", graph_structural_sample(g, 1, 0.85)); + ADAPT("compute_merw_psi", graph_compute_merw_psi(g, 0.85, 100, 1e-8)); + + /* Mutating ops: non-stationary (each call grows the graph), so they can't be + * batch-sampled i.i.d. Keep a fixed single-op timed count; report distribution. */ + const size_t KM = 2000; + u64 *noff = malloc(KM * sizeof *noff); + for (size_t i = 0; i < KM; i++) { int nl = snprintf(nm, sizeof nm, "new-%zu", i); + u64 t0 = tsc_begin(); u64 r = graph_create_entity(g, (const u8 *)nm, (u16)nl, (const u8 *)"NewType", 7, now); u64 t1 = tsc_end(); + noff[i] = r; samp[i] = (double)(t1 - t0); } + emit_op("create_entity", samp, KM); + + for (size_t i = 0; i < KM; i++) { u64 a = noff[i], b = noff[xs() % KM]; + u64 t0 = tsc_begin(); graph_create_relation(g, a, b, (const u8 *)"newrel", 6, now); u64 t1 = tsc_end(); + samp[i] = (double)(t1 - t0); } + emit_op("create_relation", samp, KM); + + printf("\n }\n}\n"); + + for (u32 k = 0; k < POOL; k++) free(pat[k]); + for (size_t i = 0; i < N; i++) free(names[i]); + for (int t = 0; t < 20; t++) free(tynames[t]); + free(idx); free(idx2); free(tpix); free(seed); free(pat); free(names); free(nlen); + free(samp); free(out); free(off); free(noff); + graph_close(g); st_close(st); + return 0; +} diff --git a/scripts/bench-aggregate.mjs b/scripts/bench-aggregate.mjs new file mode 100644 index 0000000..62210ad --- /dev/null +++ b/scripts/bench-aggregate.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +/* + * Aggregate N op-bench run JSONs into ONE sample line for the bench-history + * NDJSON time-series. Keeps the `reference` op value alongside the per-op medians + * so downstream creep analysis can frequency-normalize across runners and time. + * + * node scripts/bench-aggregate.mjs --runs r1.json,r2.json,... --sha --date [--metric min] + * + * Emits one line: {"sha","date","reference",,"ops":{op:,...}} + */ +import { readFileSync } from 'fs'; + +function flag(name, def) { + const i = process.argv.indexOf(name); + return i >= 0 ? process.argv[i + 1] : def; +} +const files = (flag('--runs', '') || '').split(',').filter(Boolean); +const sha = flag('--sha', 'unknown'); +const date = flag('--date', 'unknown'); +const metric = flag('--metric', 'p50'); +if (!files.length) { + console.error('usage: bench-aggregate --runs r1.json[,r2,...] --sha --date [--metric min]'); + process.exit(2); +} + +function median(a) { + const s = [...a].sort((x, y) => x - y); + const n = s.length; + if (!n) return 0; + return n % 2 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2; +} + +const runs = files.map((f) => JSON.parse(readFileSync(f, 'utf8')).ops); +const ops = {}; +for (const op of Object.keys(runs[0])) { + ops[op] = median(runs.map((r) => r[op]?.[metric]).filter((x) => x != null)); +} +const reference = ops.reference || 0; +delete ops.reference; + +process.stdout.write(`${JSON.stringify({ sha, date, reference, ops })}\n`); diff --git a/scripts/bench-compare.mjs b/scripts/bench-compare.mjs new file mode 100644 index 0000000..461c9e9 --- /dev/null +++ b/scripts/bench-compare.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/* + * Compare op-bench JSON outputs (from native/op_bench), base vs head, and flag + * per-op regressions. No committed baselines: CI runs the SAME harness against + * base and head graph sources on one runner, interleaved N times, and diffs. + * + * node scripts/bench-compare.mjs --base b1.json,b2.json --head h1.json,h2.json + * [--pct 5] [--cyc 40] [--metric min] + * + * Methodology (for noisy shared CI runners): + * - Each side may have N runs; we take the MEDIAN of the per-run `min` per op. + * - We FREQUENCY-CORRECT head to base using the `reference` op (fixed work): + * invariant TSC counts wall-clock, so the reference's cycles track CPU + * frequency; scaling head by ref_base/ref_head cancels the global turbo drift + * that makes identical code look 5-13% different. + * Exits 1 if any op regressed beyond max(cyc, pct%) — wire to a SOFT-fail check. + */ +import { readFileSync } from 'fs'; + +function flag(name, def) { + const i = process.argv.indexOf(name); + return i >= 0 ? process.argv[i + 1] : def; +} +const baseFiles = (flag('--base', '') || '').split(',').filter(Boolean); +const headFiles = (flag('--head', '') || '').split(',').filter(Boolean); +if (!baseFiles.length || !headFiles.length) { + console.error('usage: bench-compare --base b1.json[,b2,...] --head h1.json[,h2,...] [--pct N] [--cyc N] [--metric min|p50]'); + process.exit(2); +} +const pct = Number(flag('--pct', '5')); +const cycFloor = Number(flag('--cyc', '40')); // ~15 ns at ~2.6 GHz +const metric = flag('--metric', 'p50'); + +function median(a) { + const s = [...a].sort((x, y) => x - y); + const n = s.length; + if (!n) return 0; + return n % 2 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2; +} +function mergeRuns(files) { + const runs = files.map((f) => JSON.parse(readFileSync(f, 'utf8')).ops); + const out = {}; + for (const op of Object.keys(runs[0])) { + out[op] = median(runs.map((r) => r[op]?.[metric]).filter((x) => x != null)); + } + return out; +} + +const base = mergeRuns(baseFiles); +const head = mergeRuns(headFiles); + +// Frequency-correct head -> base via the reference op. +const corr = base.reference && head.reference ? base.reference / head.reference : 1; + +let regressed = false; +const rows = []; +for (const op of Object.keys(head)) { + if (op === 'reference') continue; + if (!(op in base)) { rows.push({ op, isNew: true }); continue; } + const b = base[op]; + const h = Math.round(head[op] * corr); + const d = h - b; + const dp = b ? (100 * d / b) : 0; + const thr = Math.max(cycFloor, (b * pct) / 100); + const mark = d > thr ? '\u{1F534} REGRESSION' : d < -thr ? '\u{1F7E2} faster' : ''; + if (d > thr) regressed = true; + rows.push({ op, b, h, d, dp, mark }); +} + +console.log(`### Per-op benchmark — ${metric} cycles (base vs head)\n`); +console.log(`base runs: ${baseFiles.length}, head runs: ${headFiles.length} (median of mins) · ` + + `freq-correction ×${corr.toFixed(4)} · threshold head−base > max(${cycFloor} cyc, ${pct}%)\n`); +console.log('| op | base | head* | Δ | Δ% | |'); +console.log('|---|---:|---:|---:|---:|---|'); +for (const r of rows) { + if (r.isNew) { console.log(`| \`${r.op}\` | — | — | — | — | new |`); continue; } + const sd = `${r.d >= 0 ? '+' : ''}${r.d.toFixed(1)}`; + const sp = `${r.dp >= 0 ? '+' : ''}${r.dp.toFixed(1)}%`; + console.log(`| \`${r.op}\` | ${r.b} | ${r.h} | ${sd} | ${sp} | ${r.mark} |`); +} +console.log(`\n\\* head frequency-corrected to base via the \`reference\` op.`); +console.log(regressed ? '\n**Regressions detected.**' : '\n_No regressions beyond threshold._'); +process.exit(regressed ? 1 : 0); diff --git a/scripts/bench-creep.mjs b/scripts/bench-creep.mjs new file mode 100644 index 0000000..17c90be --- /dev/null +++ b/scripts/bench-creep.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/* + * Slow-creep detector for the bench-history NDJSON time-series. The per-PR gate + * (bench-compare) only sees head-vs-base, so a change that adds a sliver per PR + * compounds invisibly. This compares the newest sample's reference-NORMALIZED + * per-op cost (op/reference — frequency-independent across runners and time) + * against a rolling baseline (median over a recent window of history). + * + * node scripts/bench-creep.mjs --history history.ndjson [--sample sample.json] + * [--pct 10] [--window 20] + * + * With --sample, that sample is the "now" and the whole file is the baseline. + * Without it, the last line of history is "now" and the rest is the baseline. + * Exits 1 if any op drifted > pct% above the rolling median (soft-fail signal). + */ +import { readFileSync } from 'fs'; + +function flag(name, def) { + const i = process.argv.indexOf(name); + return i >= 0 ? process.argv[i + 1] : def; +} +const histFile = flag('--history', ''); +const sampleFile = flag('--sample', ''); +const pct = Number(flag('--pct', '10')); +const cycFloor = Number(flag('--cyc', '40')); // absolute floor: ignore sub-noise wiggle on tiny ops +const windowN = Number(flag('--window', '20')); +if (!histFile) { + console.error('usage: bench-creep --history history.ndjson [--sample sample.json] [--pct N] [--window N]'); + process.exit(2); +} + +const lines = readFileSync(histFile, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l)); +let sample; +let baseline; +if (sampleFile) { + sample = JSON.parse(readFileSync(sampleFile, 'utf8')); + baseline = lines; +} else { + sample = lines[lines.length - 1]; + baseline = lines.slice(0, -1); +} +if (!sample || !baseline.length) { + console.log('No prior history — creep check skipped (first sample establishes the baseline).'); + process.exit(0); +} + +function median(a) { + const s = [...a].sort((x, y) => x - y); + const n = s.length; + if (!n) return 0; + return n % 2 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2; +} +// reference-normalize: op cost as a fraction of the fixed-work reference op. +function normalize(s) { + const o = {}; + for (const k of Object.keys(s.ops)) o[k] = s.reference ? s.ops[k] / s.reference : s.ops[k]; + return o; +} + +const win = baseline.slice(-windowN).map(normalize); +const now = normalize(sample); + +// Convert a normalized delta back to approximate cycles at this sample's +// frequency, so a tiny op's large % wiggle below the absolute floor is ignored. +const refNow = sample.reference || 1; +let creep = false; +const rows = []; +for (const op of Object.keys(now)) { + const base = median(win.map((w) => w[op]).filter((x) => x != null)); + if (!base) continue; + const c = now[op]; + const dp = (100 * (c - base)) / base; + const dcyc = (c - base) * refNow; + const isCreep = dp > pct && dcyc > cycFloor; + const isFaster = dp < -pct && -dcyc > cycFloor; + const mark = isCreep ? '\u{1F534} CREEP' : isFaster ? '\u{1F7E2} improved' : ''; + if (isCreep) creep = true; + rows.push({ op, base, c, dp, dcyc, mark }); +} + +console.log(`### Bench creep — normalized cost vs ${win.length}-sample rolling baseline\n`); +console.log(`sample: \`${String(sample.sha || '').slice(0, 9)}\` (${sample.date || '?'}) · flag drift > ${pct}% AND > ${cycFloor} cyc above rolling median\n`); +console.log('| op | baseline | now | Δ% | Δcyc | |'); +console.log('|---|---:|---:|---:|---:|---|'); +for (const r of rows) { + console.log(`| \`${r.op}\` | ${r.base.toFixed(4)} | ${r.c.toFixed(4)} | ${r.dp >= 0 ? '+' : ''}${r.dp.toFixed(1)}% | ${r.dcyc >= 0 ? '+' : ''}${Math.round(r.dcyc)} | ${r.mark} |`); +} +console.log(creep ? '\n**Creep detected.**' : '\n_No creep beyond threshold._'); +process.exit(creep ? 1 : 0); From eb672e63c6040b2b9a613ffbafa2dd2ecaac6579 Mon Sep 17 00:00:00 2001 From: Levalicious <24482560+Levalicious@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:29:40 -0400 Subject: [PATCH 17/17] proofs: ACSL/WP for the adjacency edge encoding + Frama-C CI gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fc_graph.c: WP/ACSL proofs (memory-model-clean abstractions, fc_index idiom) of the adjacency edge encoding that every traversal decodes. - adj_pack / adj_pack_injective: the (target<<2)|dir packing is a lossless round-trip and injective for target<2^62, dir<4. Modeled arithmetically as target*4+dir (decode /4, %4 — bit-identical for these field widths); the literal bitwise form is opaque to WP's integer model, the same abstraction boundary fc_index uses for hash32. - read_edges_model: iterating count 24-byte entries at adj+8+i*24 is memory-safe and terminating. 42/42 goals discharged by Alt-Ergo. CI (proofs.yml, path-filtered to proof changes): opam frama-c + alt-ergo, runs `make proofs`. scripts/run-proofs.sh gates the result — frama-c exits 0 even with unproved goals, so it parses "Proved goals: P / T": clean files must be 100%, fc_tree is safety-only (mf_fit_model termination needs an acyclicity measure, the documented free-list-cycle frontier). Verified locally: fc_proofs 25/25, fc_index 27/27, fc_graph 42/42, fc_tree 12/13. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EfzTKfjTRRNQ828crWoJf1 --- .github/workflows/proofs.yml | 49 ++++++++++++++++++++ native/Makefile | 19 +++++++- native/fc_graph.c | 86 ++++++++++++++++++++++++++++++++++++ scripts/run-proofs.sh | 46 +++++++++++++++++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/proofs.yml create mode 100644 native/fc_graph.c create mode 100755 scripts/run-proofs.sh diff --git a/.github/workflows/proofs.yml b/.github/workflows/proofs.yml new file mode 100644 index 0000000..e1516a2 --- /dev/null +++ b/.github/workflows/proofs.yml @@ -0,0 +1,49 @@ +name: proofs + +# Frama-C/WP proofs of the v3 store's tractable algorithms: allocator size +# quantization + open-addressing probe (fc_proofs), name-index traversal bounds +# (fc_index), free-list descent memory-safety (fc_tree), and the adjacency edge +# encoding round-trip/injectivity + edge-iteration bounds (fc_graph). These prove +# memory-model-clean abstractions of the real algorithms — no mmap region for WP +# to reason about. Only runs when the proofs (or their build) change. +on: + pull_request: + paths: + - 'native/fc_*.c' + - 'native/fc_stubs.h' + - 'native/Makefile' + - '.github/workflows/proofs.yml' + push: + branches: [master] + paths: + - 'native/fc_*.c' + - 'native/fc_stubs.h' + - 'native/Makefile' + - '.github/workflows/proofs.yml' + +permissions: + contents: read + +jobs: + wp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ocaml/setup-ocaml@v3 + with: + ocaml-compiler: "5.2" + + - name: Install Frama-C + Alt-Ergo + run: opam install -y frama-c alt-ergo + + - name: Detect provers + run: | + eval $(opam env) + why3 config detect + + - name: Run WP proofs + run: | + eval $(opam env) + frama-c -version + make -C native proofs diff --git a/native/Makefile b/native/Makefile index 51612a4..e580290 100644 --- a/native/Makefile +++ b/native/Makefile @@ -11,7 +11,7 @@ CFLAGS = -std=c11 -g -O1 -Wall -Wextra -fsanitize=address,undefined \ LIBS = -lm OUT = /tmp/mf_test -.PHONY: test verify-detector test_memfile test_stringtable test_graph test_entity bench clean +.PHONY: test verify-detector test_memfile test_stringtable test_graph test_entity bench proofs proofs-eva clean # `make test` = prove the detector fires, then run every harness with it active. test: verify-detector test_memfile test_stringtable test_graph test_entity @@ -40,5 +40,22 @@ BENCH_CFLAGS = -std=c11 -O2 -march=native -Wall -D_GNU_SOURCE -I. bench: op_bench.c graph.c stringtable.c memoryfile.c $(CC) $(BENCH_CFLAGS) $^ -lm -o $(OUT)_bench && $(OUT)_bench +# ---- Frama-C/WP + EVA proofs ---------------------------------------------- +# Memory-model-clean abstractions in fc_*.c (NEVER compiled into the build): +# allocator size-quantization + open-addressing probe (fc_proofs), name-index +# traversal bounds (fc_index), free-list descent safety (fc_tree), adjacency +# edge encoding + edge-iteration bounds (fc_graph), and an EVA pass over the +# real allocator (fc_harness). frama-c + alt-ergo + z3 (CI installs via opam). +FRAMAC ?= frama-c + +# frama-c exits 0 even with unproved goals, so the gate (scripts/run-proofs.sh) +# parses the "Proved goals: P / T" summary: clean files must be 100%, fc_tree is +# safety-only (termination = documented acyclicity frontier). +proofs: + @FRAMAC="$(FRAMAC)" bash ../scripts/run-proofs.sh + +proofs-eva: fc_harness.c memoryfile.c + $(FRAMAC) -eva -main fc_main -cpp-extra-args="-include fc_stubs.h -I." $^ + clean: rm -f $(OUT)_* diff --git a/native/fc_graph.c b/native/fc_graph.c new file mode 100644 index 0000000..c4c00ed --- /dev/null +++ b/native/fc_graph.c @@ -0,0 +1,86 @@ +/* + * ACSL/WP: adjacency edge ENCODING + edge-iteration bounds for the v3 graph store. + * Every traversal (neighbors, find_path, edges, remove_edge) decodes adjacency + * entries via this packing and walks the adjacency block, so both are load-bearing. + * + * frama-c -wp -wp-rte -wp-prover alt-ergo,z3 fc_graph.c + * + * 1) adj_pack / adj_pack_injective: a target offset and a 2-bit direction share one + * u64, ((target<<2)|dir). Proved a ROUND-TRIP (unpack o pack == identity) and + * INJECTIVE, given target fits in 62 bits and dir < 4 — no high-offset truncation, + * no two distinct (target,dir) pairs aliasing to the same packed word. + * 2) read_edges_model: iterating n = min(count,max) 24-byte entries at + * adj + 8 + i*24 (the real ADJ_HEADER_SIZE / ADJ_ENTRY_SIZE) is memory-safe — + * every 24-byte access in-bounds — and terminating, given the block fits. + * + * Reads go through spec'd accessors (rd32/rd64); the real code reads via memcpy for + * aliasing safety, so modeling the read with an in-bounds contract keeps the proof + * about the OFFSET ARITHMETIC, not libc memcpy separation (same idiom as fc_index). + */ +#include + +#define ADJ_HEADER_SIZE 8u +#define ADJ_ENTRY_SIZE 24u +#define DIR_MASK 3u +#define TARGET_MAX 0x3fffffffffffffffULL /* 2^62 - 1: survives << 2 losslessly */ + +/* ---- 1. adjacency encoding: round-trip + injectivity (pure arithmetic) ---- */ + +/* The real encoding is ((target<<2)|(dir&3u)). With dir < 4 the two fields occupy + * disjoint bit ranges, so it equals target*4 + dir exactly — and target < 2^62 + * means the shift never truncates. We prove the ALGORITHM (lossless round-trip + + * injectivity) over that arithmetic form; decode >>2 / &3 are the unsigned /4 / %4 + * proved below. Same abstraction boundary as fc_index modeling hash32 % bc. */ +/*@ requires target <= TARGET_MAX; + @ requires dir <= DIR_MASK; + @ assigns \nothing; + @ ensures \result == target * 4 + dir; + @ ensures \result / 4 == target; // decode target (== packed >> 2) + @ ensures \result % 4 == dir; // decode direction(== packed & 3) + @*/ +uint64_t adj_pack(uint64_t target, uint64_t dir) { + return target * 4 + dir; +} + +/*@ requires t1 <= TARGET_MAX && d1 <= DIR_MASK; + @ requires t2 <= TARGET_MAX && d2 <= DIR_MASK; + @ assigns \nothing; + @ ensures \result != 0 <==> (t1 == t2 && d1 == d2); + @*/ +int adj_pack_injective(uint64_t t1, uint64_t d1, uint64_t t2, uint64_t d2) { + return adj_pack(t1, d1) == adj_pack(t2, d2); +} + +/* ---- 2. edge-iteration bounds (real adj-block offset arithmetic) ---- */ + +/*@ requires o + 8 <= msize; + @ requires \valid_read(mem + (0 .. msize - 1)); + @ assigns \nothing; */ +uint64_t rd64(unsigned char *mem, uint64_t msize, uint64_t o); + +/*@ requires o + 4 <= msize; + @ requires \valid_read(mem + (0 .. msize - 1)); + @ assigns \nothing; */ +uint32_t rd32(unsigned char *mem, uint64_t msize, uint64_t o); + +/*@ requires \valid_read(mem + (0 .. msize - 1)); + @ requires n <= count; + @ requires adj + ADJ_HEADER_SIZE + (uint64_t)count * ADJ_ENTRY_SIZE <= msize; + @ assigns \nothing; + @*/ +void read_edges_model(unsigned char *mem, uint64_t msize, uint64_t adj, + uint32_t count, uint32_t n) { + /*@ loop invariant 0 <= i <= n; + @ loop assigns i; + @ loop variant n - i; + @*/ + for (uint32_t i = 0; i < n; i++) { + uint64_t base = adj + ADJ_HEADER_SIZE + (uint64_t)i * ADJ_ENTRY_SIZE; + //@ assert base + ADJ_ENTRY_SIZE <= msize; + uint64_t packed = rd64(mem, msize, base + 0); + uint32_t rel = rd32(mem, msize, base + 8); + uint64_t mtime = rd64(mem, msize, base + 16); + //@ assert (packed >> 2) <= TARGET_MAX; // decoded target always in packable range + (void)packed; (void)rel; (void)mtime; + } +} diff --git a/scripts/run-proofs.sh b/scripts/run-proofs.sh new file mode 100755 index 0000000..801c68d --- /dev/null +++ b/scripts/run-proofs.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Frama-C/WP proof gate for the v3 store. +# +# frama-c exits 0 even when WP goals are unproved, so we PARSE the per-file +# "Proved goals: P / T" summary and gate on it: +# - clean files must be FULLY proved (P == T); +# - fc_tree is safety-only — mf_fit_model's termination needs an acyclicity +# measure (the documented free-list-cycle frontier), so we require its +# safety/functional goals only (P >= 12) and tolerate the 1 terminates goal. +# +# Run from anywhere; CI calls it via `make -C native proofs`. +set -u +cd "$(dirname "$0")/../native" || exit 2 +FRAMAC="${FRAMAC:-frama-c}" +FLAGS=(-wp -wp-rte -wp-prover alt-ergo -wp-timeout 30) +rc=0 + +gate() { # file mode(strict|min) [minval] + local f=$1 mode=$2 min=${3:-0} + echo "===== $f =====" + local out line + out=$("$FRAMAC" "${FLAGS[@]}" "$f" 2>&1) + line=$(echo "$out" | grep -m1 'Proved goals' || true) + echo " ${line:-}" + if [ -z "$line" ]; then echo " FAIL: no proof summary for $f"; rc=1; return; fi + # shellcheck disable=SC2046 + set -- $(echo "$line" | grep -oE '[0-9]+ / [0-9]+' | tr '/' ' ') + local p=${1:-0} t=${2:-0} + if [ "$mode" = strict ]; then + if [ "$p" != "$t" ]; then + echo " FAIL: $f proved $p/$t (expected all)" + echo "$out" | grep -i 'Timeout\]' | sed 's/^/ /' + rc=1 + fi + else + if [ "$p" -lt "$min" ]; then echo " FAIL: $f proved $p < $min safety goals"; rc=1; fi + fi +} + +gate fc_proofs.c strict +gate fc_index.c strict +gate fc_graph.c strict +gate fc_tree.c min 12 # safety-only; termination = documented acyclicity frontier + +if [ "$rc" = 0 ]; then echo "ALL PROOF GATES PASSED"; fi +exit "$rc"