Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
932a3dc
ci: run the api-contract proof-pack against the built API (#93)
romiluz13 Oct 18, 2026
27e1253
scripts: type-check benchmark scripts and tests in check-types (#92)
romiluz13 Oct 18, 2026
f2308f8
ci: arm the proof-pack with the Voyage key and refuse fallbacks (#93)
romiluz13 Oct 18, 2026
3ae2b2f
benchmark: admit LongMemEval with numeric gold, occurrence ids and ad…
romiluz13 Oct 18, 2026
7aa166f
benchmark: carry adapter rejections through checkpoints and resume (#94)
romiluz13 Oct 18, 2026
24a8e57
benchmark: reject a LongMemEval question with no ingestible turn as q…
romiluz13 Oct 18, 2026
770fb7c
benchmark: label LongMemEval questions without evidence as no-evidenc…
romiluz13 Oct 18, 2026
8eaba81
benchmark: admit LoCoMo with a full session walk, typed rejections an…
romiluz13 Oct 18, 2026
5459207
benchmark: keep LoCoMo abstention and no-evidence items out of retrie…
romiluz13 Oct 18, 2026
579dc95
benchmark: split LoCoMo turn tests and drop a cast in evidence sessio…
romiluz13 Oct 18, 2026
562407d
engine: add opt-in search visibility barrier on writes (#101)
romiluz13 Oct 18, 2026
2bf2549
engine: report quarantined barrier writes without probing (#101)
romiluz13 Oct 18, 2026
ec4ac4b
engine: typed search outcomes; mongoSearch runs one planned tier (#101)
romiluz13 Oct 18, 2026
f3f48a1
benchmark: omit sessionIndex on LoCoMo session-invalid-key entries (#95)
romiluz13 Oct 18, 2026
16442ca
engine: log failed barrier probes with bounded fields (#101)
romiluz13 Oct 18, 2026
0b23569
engine: structured and procedure $search tiers replace classic $text …
romiluz13 Oct 18, 2026
b8898d3
engine: value-free lane outcomes end to end; no error-triggered fallb…
romiluz13 Oct 18, 2026
9a89ba1
Merge branch 'm1/b2-scripts-typecheck' into integration/m1-20261018
romiluz13 Oct 18, 2026
e6cf25b
Merge branch 'm1/a-locomo-admission' into integration/m1-20261018
romiluz13 Oct 18, 2026
625b627
Merge branch 'm1/c-engine-barrier' into integration/m1-20261018
romiluz13 Oct 18, 2026
4aca968
Merge branch 'm1/c-engine-lanes' into integration/m1-20261018
romiluz13 Oct 18, 2026
ba726d1
api: map search outcomes to the fixed HTTP statuses (#101)
romiluz13 Oct 18, 2026
47b6de7
client: carry search outcomes on responses, errors and silent markers…
romiluz13 Oct 18, 2026
18b49ed
api: derive the boot text lane from the engine's $text index rule (#101)
romiluz13 Oct 18, 2026
3c2d90d
api: document search outcomes and failure statuses in OpenAPI (#101)
romiluz13 Oct 18, 2026
3cd46b6
engine: charge search embed and aggregation pairs atomically (#101)
romiluz13 Oct 18, 2026
ab04af4
engine: per-lane search cost table for ledger reservations (#101)
romiluz13 Oct 18, 2026
f9d3ba9
Merge branch 'm1/c-api-outcomes' into integration/m1-20261018
romiluz13 Oct 18, 2026
452e210
Merge branch 'm1/b1-proofpack-ci' into m1/b1-proofpack-final
romiluz13 Oct 18, 2026
7386d96
engine: reserve required search lanes first; reuse equivalent retriev…
romiluz13 Oct 18, 2026
dcca132
proof-pack: correct the search and searchDetailed requests, opt write…
romiluz13 Oct 18, 2026
7e8e608
engine: 1204 E passes share one ledger; key the resolved result limit…
romiluz13 Oct 18, 2026
3377d71
ci: run the proof-pack in its own keyed job with failure diagnostics …
romiluz13 Oct 18, 2026
4483c44
engine: manager legacy re-runs and direct KB search on the request le…
romiluz13 Oct 18, 2026
73f7d75
Merge branch 'm1/b1-proofpack-final' into integration/m1-20261018
romiluz13 Oct 18, 2026
8b88f2e
Merge branch 'm1/c-engine-ledger' into integration/m1-20261018
romiluz13 Oct 18, 2026
d822ca7
proof-pack: require the agent scope, not only its scopeRef, on the se…
romiluz13 Oct 18, 2026
a8c054e
Merge branch 'm1/b1-proofpack-final' into integration/m1-20261018
romiluz13 Oct 18, 2026
246294d
ci: run the proof-pack API with strict search-index readiness (#93)
romiluz13 Oct 18, 2026
62e2171
ci: scrub the proof-pack API log before printing or upload (#93)
romiluz13 Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ jobs:
e2e:
name: E2E against real MongoDB (tier A)
runs-on: ubuntu-latest
timeout-minutes: 25
services:
# Keyless on purpose: tier A never embeds, and the search-barrier suite
# expects a service without a provider key.
mongodb:
image: mongodb/mongodb-atlas-local:preview
ports:
Expand Down Expand Up @@ -113,3 +116,105 @@ jobs:
--require-suite mongodb-projection-repair.e2e.test.ts
--require-suite mongodb-temporal-validity.e2e.test.ts
--require-suite mongodb-memory-jobs.e2e.test.ts
--require-suite mongodb-search-barrier.e2e.test.ts

# The api-contract lane runs in its own job so paid embeddings happen only
# here: Rom's grant is this PR job, at most 60 barrier probe embeddings per
# run, on the existing VOYAGE_API_KEY secret.
proof-pack:
name: API contract proof-pack (keyed MongoDB)
runs-on: ubuntu-latest
timeout-minutes: 25
services:
mongodb:
image: mongodb/mongodb-atlas-local:preview
ports:
- 27017:27017
env:
MONGODB_ATLAS_TELEMETRY_ENABLE: "false"
# Auto-embeddings are served by mongot inside the container, so the
# Atlas Model key must be on the container for the proof-pack's
# vector and hybrid lanes and its barrier probes.
VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }}
options: >-
--health-cmd "/usr/local/bin/runner healthcheck"
--health-interval 10s
--health-timeout 10s
--health-retries 20
--health-start-period 40s
steps:
# Zero fallbacks: without the key (a fork PR, or the secret unset) the
# API would silently serve search from $text, so the job fails here
# instead of skipping or going green on the fallback.
- name: Assert Voyage key is armed for the proof-pack (no fallback)
env:
VOYAGE_KEY_PRESENT: ${{ secrets.VOYAGE_API_KEY != '' }}
run: |
if [[ "${VOYAGE_KEY_PRESENT}" != "true" ]]; then
echo "::error::VOYAGE_API_KEY secret is empty: the MongoDB service cannot embed, so the proof-pack would run on the \$text fallback"
exit 1
fi
echo "VOYAGE_API_KEY secret present on the MongoDB service container"

- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json

# Node 22 matches the API image (apps/api/Dockerfile), which runs the
# built apps/api/dist/server.js that the proof-pack step starts.
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build
run: bun run build

# The unweakened 15-check proof-pack against this PR's built API on
# loopback, with its own database, agent and API key. The API process
# gets no provider key; embeddings come from the MongoDB service. A
# startup failure, a missing check, a failed check or a degraded search
# lane fails the job.
- name: API contract proof-pack (15 checks)
id: proof_pack
timeout-minutes: 10
env:
MEMONGO_MONGODB_URI: mongodb://127.0.0.1:27017/?directConnection=true
PROOF_PACK_OUT_DIR: ${{ runner.temp }}/proof-pack
run: bash scripts/ci/run-proof-pack.sh

# Failure-only and diagnostic only: the service container's log (mongod
# and mongot) and the proof database's search-index status, each bounded
# and scrubbed of URIs, keys and document text before upload. Index status
# is never a visibility signal. The raw log never enters the artifact.
- name: Collect proof-pack diagnostics (service log, search-index status)
if: failure() && steps.proof_pack.outcome == 'failure'
timeout-minutes: 3
env:
MEMONGO_MONGODB_URI: mongodb://127.0.0.1:27017/?directConnection=true
PROOF_PACK_OUT_DIR: ${{ runner.temp }}/proof-pack
SERVICE_CONTAINER_ID: ${{ job.services.mongodb.id }}
# Read only so the scrubber can remove the key verbatim.
VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }}
run: |
raw_log="$(mktemp)"
status=0
docker logs --tail 20000 "${SERVICE_CONTAINER_ID}" >"${raw_log}" 2>&1 || status=$?
bun scripts/ci/collect-proof-pack-diagnostics.ts "${raw_log}" "${PROOF_PACK_OUT_DIR}/diagnostics" "${PROOF_PACK_OUT_DIR}/database.txt" || status=$?
rm -f "${raw_log}"
exit "${status}"

- name: Upload proof-pack evidence (API log, PID, verdict, diagnostics)
if: failure() && steps.proof_pack.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: proof-pack-evidence
path: ${{ runner.temp }}/proof-pack
if-no-files-found: error
21 changes: 14 additions & 7 deletions apps/api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ const bridgeMocks = vi.hoisted(() => ({
buildMemongoConfig: vi.fn(),
}))

vi.mock("@memongo/memory-bridge", () => bridgeMocks)
// #101: the boot capability table derives its classic $text lane from the
// engine's own index rule, so the real rule rides along with the mocks.
vi.mock("@memongo/memory-bridge", async (importOriginal) => ({
...bridgeMocks,
shouldEnsureTextFallbackIndexes: (
await importOriginal<typeof import("@memongo/memory-bridge")>()
).shouldEnsureTextFallbackIndexes,
}))

import { createApp, parseScopedApiKeyPolicies } from "./app.js"
import { MEMONGO_API_VERSION } from "./version.js"
Expand Down Expand Up @@ -537,7 +544,7 @@ describe("createApp", () => {
hybrid: true,
vector: true,
keyword: true,
text: true,
text: false,
})
})

Expand All @@ -546,16 +553,16 @@ describe("createApp", () => {
hybrid: false,
vector: false,
keyword: true,
text: true,
text: false,
})
})

it("keeps only the $text fallback when capabilities are null", () => {
it("reports every lane unavailable when capabilities are null", () => {
expect(deriveSearchLanes(null)).toEqual({
hybrid: false,
vector: false,
keyword: false,
text: true,
text: false,
})
})

Expand All @@ -566,7 +573,7 @@ describe("createApp", () => {
expect(table).toContain("hybrid: unavailable")
expect(table).toContain("vector: unavailable")
expect(table).toContain("keyword: available")
expect(table).toContain("text: available")
expect(table).toContain("text: unavailable")
expect(table).toContain("DEGRADED")
})

Expand Down Expand Up @@ -625,7 +632,7 @@ describe("createApp", () => {
hybrid: false,
vector: false,
keyword: false,
text: true,
text: false,
})
})
})
Expand Down
54 changes: 50 additions & 4 deletions apps/api/src/lib/capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const mongoUri = (password: string) =>
["mongodb://probe-user:", password, "@mongo.internal:27017/memongo"].join("")

describe("deriveSearchLanes", () => {
it("maps bridge capabilities onto lanes", () => {
// #101 section 3 step 7: `text` is the classic $text lane, which exists
// only where the engine creates its index — deployments without Atlas
// Search (the shouldEnsureTextFallbackIndexes rule).
it("reports no classic $text lane on an Atlas Search deployment", () => {
expect(
deriveSearchLanes({
vectorSearch: true,
Expand All @@ -23,12 +26,28 @@ describe("deriveSearchLanes", () => {
storedSource: false,
vectorIndexMethod: false,
}),
).toEqual({ hybrid: true, vector: true, keyword: true, text: true })
).toEqual({ hybrid: true, vector: true, keyword: true, text: false })
})

it("reports the classic $text lane on a deployment without Atlas Search", () => {
expect(
deriveSearchLanes({
vectorSearch: false,
textSearch: false,
scoreFusion: false,
rankFusion: false,
storedSource: false,
vectorIndexMethod: false,
}),
).toEqual({ hybrid: false, vector: false, keyword: false, text: true })
})

it("reports every lane unavailable when the probe is unknown", () => {
expect(deriveSearchLanes(null)).toEqual({
hybrid: false,
vector: false,
keyword: false,
text: true,
text: false,
})
})

Expand Down Expand Up @@ -58,6 +77,33 @@ describe("formatCapabilityTable", () => {
expect(table).not.toContain("probe failed")
})

it("treats a healthy Search deployment without classic $text as fully available", () => {
const table = formatCapabilityTable({
hybrid: true,
vector: true,
keyword: true,
text: false,
})
expect(table).toContain("hybrid: available")
expect(table).toContain("vector: available")
expect(table).toContain(
"text: unavailable (classic $text, chunks/KB only)",
)
expect(table).toContain("all retrieval lanes available")
expect(table).not.toContain("DEGRADED")
})

it("never claims a silent $text fallback when degraded", () => {
const table = formatCapabilityTable({
hybrid: false,
vector: false,
keyword: false,
text: true,
})
expect(table).toContain("DEGRADED")
expect(table).not.toContain("silently fall back")
})

it("includes the probe failure detail when present", () => {
const table = formatCapabilityTable(
{ hybrid: false, vector: false, keyword: false, text: true },
Expand Down Expand Up @@ -150,7 +196,7 @@ describe("probeBootCapabilities (C-002: probe diagnostics never carry credential
hybrid: true,
vector: true,
keyword: true,
text: true,
text: false,
})
expect(report.probeError).toBeUndefined()
})
Expand Down
29 changes: 17 additions & 12 deletions apps/api/src/lib/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import type { MemongoBridgeCapabilities } from "@memongo/memory-bridge"
import {
type MemongoBridgeCapabilities,
shouldEnsureTextFallbackIndexes,
} from "@memongo/memory-bridge"
import { formatErrorMessage, redactSensitiveText } from "@memongo/lib"

/**
* P1.9 boot-time search-lane visibility. When Atlas Search/vector indexes
* cannot be created, the engine warns and silently degrades every query to
* the `$text` last resort. These helpers make the degradation visible at
* boot: a capability table is logged once, and MEMONGO_REQUIRE_VECTOR=1
* refuses to boot when the vector lane is unavailable.
* cannot be created, searches that need those lanes report them per call as
* not-applicable or lane-unavailable outcomes (#101). These helpers make the
* deployment's lanes visible at boot: a capability table is logged once, and
* MEMONGO_REQUIRE_VECTOR=1 refuses to boot when the vector lane is
* unavailable.
*
* Lane model (derived from the engine's DetectedCapabilities via the bridge):
* - vector: $vectorSearch serving index exists and is queryable
* - keyword: Atlas Search $search serving index exists and is queryable
* - hybrid: vector + keyword (the engine falls back to js-merge fusion
* when server-side score/rank fusion stages are absent, so the
* lane needs both retrieval legs, not the fusion stages)
* - text: standard MongoDB $text index last resort — always available
* - text: classic MongoDB $text index (chunks and KB only), which the
* engine creates only where Atlas Search is absent
*
* All functions are pure (or take an injected probe/logger) so the unit
* suite can drive them without booting a server or a live Mongo.
Expand All @@ -35,9 +40,9 @@ export function deriveSearchLanes(
hybrid: vector && keyword,
vector,
keyword,
// Standard $text indexes are a core MongoDB feature, not Atlas Search —
// the last-resort lane exists on every deployment.
text: true,
// #101: the lane exists only where the engine creates its index, by the
// same rule; an unknown probe reports it unavailable.
text: caps != null && shouldEnsureTextFallbackIndexes(caps),
}
}

Expand All @@ -51,13 +56,13 @@ export function formatCapabilityTable(
` hybrid: ${status(lanes.hybrid)}`,
` vector: ${status(lanes.vector)}`,
` keyword: ${status(lanes.keyword)}`,
` text: ${status(lanes.text)} (standard $text fallback)`,
` text: ${status(lanes.text)} (classic $text, chunks/KB only)`,
]
if (lanes.hybrid && lanes.vector && lanes.keyword && lanes.text) {
if (lanes.hybrid && lanes.vector && lanes.keyword) {
lines.push(" all retrieval lanes available")
} else {
lines.push(
" DEGRADED: queries on unavailable lanes silently fall back to $text",
" DEGRADED: searches report unavailable lanes as typed outcomes; no lane falls back to $text",
)
}
if (probeError) {
Expand Down
27 changes: 19 additions & 8 deletions apps/api/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,30 @@ export function isDependencyUnavailableError(err: unknown): boolean {
export function internalError(c: Context, err: unknown, code: string) {
const requestId = c.get("requestId") ?? "no-request-id"
let errorCode: unknown
let commitState: unknown
try {
if (err && typeof err === "object") {
errorCode = Object.getOwnPropertyDescriptor(err, "code")?.value
commitState = Object.getOwnPropertyDescriptor(err, "commitState")?.value
}
} catch {
// A proxy descriptor trap must not replace the original failure.
}
// #101: a search-barrier write whose commit outcome is unknown says so;
// the body claims neither committed nor searchable.
const respond = (
status: 500 | 503,
responseCode: string,
message: string,
) => {
const body = apiErrorJson(responseCode, message)
return commitState === "unknown"
? c.json(
{ error: { ...body.error, commitState: "unknown" as const } },
status,
)
: c.json(body, status)
}
console.error(
JSON.stringify({
level: "error",
Expand All @@ -94,17 +111,11 @@ export function internalError(c: Context, err: unknown, code: string) {
}),
)
if (isDependencyUnavailableError(err)) {
return jsonError(
c,
return respond(
503,
"SERVICE_UNAVAILABLE",
`dependency unavailable (request id: ${requestId})`,
)
}
return jsonError(
c,
500,
code,
`internal server error (request id: ${requestId})`,
)
return respond(500, code, `internal server error (request id: ${requestId})`)
}
Loading
Loading