HalofyIndex
AI agent governance · MCP/HTTP memory kernel

检索插件可替换,但读取边界必须由 ScopedView 固定

Halofy 检索源码拆解:ScopedView 只读视图、ContextAllocator lane packing、FaultHandler L2/L3 阈值、BM25 brownout 与 driver score calibration。

观察结论

它不是又一个 prompt memory 插件,而是把身份、ACL、写入验证、检索驱动、审计链和合规删除放进同一个内核边界。

4
allocator lanes
0.35
L2 floor
0.15
L3 floor
BM25
brownout
01 / driver

Driver 只拿 refs,不拿数据库

源码判断

src/driver/view.ts:1 ScopedView 文件头声明它是 driver 读取的唯一世界。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/driver/view.ts:140 whole-word matcher 降低实体名误召回。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/driver/view.ts:181 candidates 带 validOnly/access/sourceProjection。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/driver/view.ts:220 vectorSearch 继续沿用 access 和 sourceIds。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/driver/view.ts:242 sourceProjection 下 concepts fail closed。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/driver/view.ts:260 entityFacts 从 scoped fact pool 开始构造实体跳数。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/driver/view.ts:181-258source
 181export function buildScopedView<TDb>(
 182  deps: ScopedViewDeps<TDb>,
 183  namespace: Namespace,
 184  access?: AccessSubject,
 185  sourceProjection?: RetrievalSourceProjection,
 186): ScopedView {
 187  return {
 188    namespace,
 189
 190    async candidates(limit = DEFAULT_CANDIDATE_LIMIT) {
 191      const rows = await deps.index.listCandidates(namespace, {
 192        validOnly: true,
 193        limit,
 194        access,
 195        sourceIds: sourceProjection?.sourceIds,
 196      });
 197      return rows.map((r) => ({
 198        ref: r.id,
 199        content: retrievalContent(r, sourceProjection),
 200      }));
 201    },
 202
 203    ...(deps.index.listLexicalCandidates
 204      ? {
 205          async lexicalCandidates(query: string, limit: number) {
 206            const rows = await deps.index.listLexicalCandidates!(namespace, query, {
 207              validOnly: true,
 208              limit,
 209              access,
 210              sourceIds: sourceProjection?.sourceIds,
 211            });
 212            return rows.map((r) => ({
 213              ref: r.id,
 214              content: retrievalContent(r, sourceProjection),
 215            }));
 216          },
 217        }
 218      : {}),
 219
 220    async vectorSearch(embedding, k) {
 221      const hits = await deps.index.vectorSearch(
 222        namespace,
 223        embedding,
 224        k,
 225        true,
 226        false,
 227        false,
 228        access,
 229        sourceProjection?.sourceIds,
 230      );
 231      return hits.map((h) => ({
 232        ref: h.obj.id,
 233        content: retrievalContent(h.obj, sourceProjection),
 234        score: h.similarity,
 235      }));
 236    },
 237
 238    async embed(texts) {
 239      return deps.embedder.embed(texts);
 240    },
 241
 242    async concepts(query, k) {
 243      // L3 concepts do not retain per-source provenance. A source-scoped view
 244      // therefore fails closed instead of allowing a future driver to widen
 245      // the result set through the cold tier.
 246      if (sourceProjection?.sourceIds !== undefined) return [];
 247      return deps.cold.searchConcepts(deps.db, deps.embedder, namespace, query, k);
 248    },
 249
 250    async entityFacts(mention, hops) {
 251      const needle = mention.trim().toLowerCase();
 252      if (needle.length === 0) return [];
 253
 254      const entities = await deps.entities.listEntities(namespace);
 255      const seeds = entities.filter((e) =>
 256        lowerNames(e).some((n) => n === needle),
 257      );
 258      if (seeds.length === 0) return [];
src/driver/view.ts:260-305source
 260      // ACL-scoped fact pool (delegated to the index — see module header).
 261      const pool = await deps.index.listCandidates(namespace, {
 262        validOnly: true,
 263        limit: DEFAULT_CANDIDATE_LIMIT,
 264        access,
 265        sourceIds: sourceProjection?.sourceIds,
 266      });
 267      const docs = pool.map((row) => {
 268        const content = retrievalContent(row, sourceProjection);
 269        return { ref: row.id, content, lower: content.toLowerCase() };
 270      });
 271
 272      const out: { ref: string; content: string; entity: string }[] = [];
 273      const seen = new Set<string>();
 274
 275      // Hop 1: facts mentioning a seed entity (whole-word, ci).
 276      const hop1Lower: string[] = [];
 277      for (const seed of seeds) {
 278        const matchers = nameMatchers(lowerNames(seed));
 279        for (const d of docs) {
 280          if (!mentions(d.lower, matchers)) continue;
 281          if (!seen.has(d.ref)) {
 282            seen.add(d.ref);
 283            out.push({ ref: d.ref, content: d.content, entity: seed.canonicalName });
 284            hop1Lower.push(d.lower);
 285          }
 286        }
 287      }
 288
 289      if (hops === 2 && hop1Lower.length > 0) {
 290        // Hop 2: entities co-mentioned in hop-1 facts → their facts.
 291        const seedNames = new Set(seeds.map((s) => s.canonicalName));
 292        for (const other of entities) {
 293          if (seedNames.has(other.canonicalName)) continue;
 294          const matchers = nameMatchers(lowerNames(other));
 295          if (!hop1Lower.some((c) => mentions(c, matchers))) continue;
 296          for (const d of docs) {
 297            if (seen.has(d.ref) || !mentions(d.lower, matchers)) continue;
 298            seen.add(d.ref);
 299            out.push({ ref: d.ref, content: d.content, entity: other.canonicalName });
 300          }
 301        }
 302      }
 303
 304      return out;
 305    },
02 / allocator

工作集按治理优先级贪心打包

源码判断

src/allocator/index.ts:7 policy-directive 作为 lane 0 获得预留 token slice。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/allocator/index.ts:125 tryAdd 用预算和 ref 去重控制工作集。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/allocator/index.ts:157 governed stable pins 在 legacy path pins 前处理。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/allocator/index.ts:207 scope-memory lane 只使用有效且曾访问的候选。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/allocator/index.ts:232 predicted-relevant 只在 taskHint 非空时启动。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/allocator/index.ts:285 assemble audit detail 记录 blocks、tokens 与 policy digest。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/allocator/index.ts:110-177source
 110  async assemble(args: AssembleArgs): Promise<WorkingSet> {
 111    const t0 = performance.now();
 112    const { namespace, policy, taskHint } = args;
 113    // Defensive: some callers (syscall layer) route audit attribution
 114    // themselves and may omit actor/agentId at runtime.
 115    const actor = args.actor ?? "unknown";
 116    const agentId = args.agentId ?? "unknown";
 117    const budget = policy.tokenBudget;
 118
 119    let totalTokens = 0;
 120    let rejectedPins = 0;
 121    let brownoutDetail = "";
 122    const selected: ContextBlock[] = [];
 123    const usedRefs = new Set<string>();
 124
 125    const tryAdd = (block: ContextBlock): void => {
 126      if (usedRefs.has(block.ref)) return;
 127      if (totalTokens + block.tokenEstimate > budget) return; // never split
 128      selected.push(block);
 129      usedRefs.add(block.ref);
 130      totalTokens += block.tokenEstimate;
 131    };
 132
 133    // -- lane 0: governed policy directives ---------------------------------
 134    // Guardrails are packed first out of a reserved slice of the budget, so
 135    // no amount of recall can push a policy out of the working set. The
 136    // reserve is a CEILING, not an allotment: unused policy tokens stay
 137    // available to the ordinary lanes below.
 138    let policySet: PolicyDirectiveSet | null = null;
 139    if (this.deps.policyDirectives) {
 140      try {
 141        const loaded = await this.deps.policyDirectives.load(
 142          namespace,
 143          policyTokenReserve(budget),
 144        );
 145        if (loaded.documents.length > 0) {
 146          policySet = loaded;
 147          for (const block of loaded.blocks) tryAdd(block);
 148        }
 149      } catch {
 150        // Brownout (kernel rule 5): an unreadable guardrail lane degrades the
 151        // working set, it never fails the agent's call. The audit detail below
 152        // records that policy was unavailable for this assembly.
 153        brownoutDetail += " brownout=policy_unavailable";
 154      }
 155    }
 156
 157    // -- lane 1a: governed stable pins (durable creation order) --------------
 158    if (this.deps.pins) {
 159      try {
 160        const governed = await this.deps.pins.resolve(namespace, args.access);
 161        rejectedPins += governed.rejected;
 162        for (let i = 0; i < governed.pins.length; i++) {
 163          const pin = governed.pins[i];
 164          tryAdd({
 165            kind: pin.kind === "memory" ? "pinned-memory" : "pinned-concept",
 166            ref: pin.ref,
 167            content: pin.content,
 168            tokenEstimate: estimateTokens(pin.content),
 169            orderKey: `0-pin-0-governed-${String(i).padStart(6, "0")}-${pin.pinId}`,
 170          });
 171        }
 172      } catch {
 173        // Brownout: pin metadata or its backing store may be unavailable. The
 174        // compatibility and ordinary lanes still provide a working set.
 175        rejectedPins++;
 176      }
 177    }
src/allocator/index.ts:207-267source
 207    // -- lane 2: most-recently-accessed valid scope memories -----------------
 208    const rows = await this.deps.index.listCandidates(namespace, {
 209      validOnly: true,
 210      limit: CANDIDATE_LIMIT,
 211      access: args.access,
 212    });
 213    const now = Date.now();
 214    const accessed = rows
 215      .filter((r) => r.tValidTo === null || Date.parse(r.tValidTo) > now)
 216      .filter(
 217        (r) =>
 218          r.telemetry.lastAccessed !== null || r.telemetry.accessCount > 0,
 219      )
 220      .sort(byRecencyDesc)
 221      .slice(0, SCOPE_LANE_LIMIT);
 222    for (const row of accessed) {
 223      tryAdd({
 224        kind: "scope-memory",
 225        ref: row.id,
 226        content: row.content,
 227        tokenEstimate: estimateTokens(row.content),
 228        orderKey: `1-scope-${row.id}`,
 229      });
 230    }
 231
 232    // -- lane 3: predicted-relevant (only with a task hint) ------------------
 233    if (taskHint && taskHint.trim().length > 0) {
 234      let hits: HybridHit[];
 235      try {
 236        hits = await hybridSearchL2({
 237          index: this.deps.index,
 238          embedder: this.deps.embedder,
 239          namespace,
 240          query: taskHint,
 241          k: PREDICTED_K,
 242          candidateLimit: CANDIDATE_LIMIT,
 243          access: args.access,
 244        });
 245      } catch {
 246        const scores = bm25Scores(
 247          taskHint,
 248          rows.map((row) => ({ ref: row.id, content: row.content })),
 249        );
 250        hits = rows
 251          .map((row) => ({ row, score: saturate(scores.get(row.id) ?? 0) }))
 252          .filter((hit) => hit.score > 0)
 253          .sort((a, b) => b.score - a.score || a.row.id.localeCompare(b.row.id))
 254          .slice(0, PREDICTED_K);
 255        brownoutDetail += " brownout=embedding_unavailable";
 256      }
 257      for (const hit of hits) {
 258        if (hit.score <= 0) continue;
 259        tryAdd({
 260          kind: "predicted-relevant",
 261          ref: hit.row.id,
 262          content: hit.row.content,
 263          tokenEstimate: estimateTokens(hit.row.content),
 264          orderKey: `2-rel-${hit.row.id}`,
 265        });
 266      }
 267    }
src/allocator/index.ts:285-356source
 285    const assembledAt = new Date().toISOString();
 286    const event = {
 287      ts: assembledAt,
 288      actor,
 289      agentId,
 290      op: "assemble",
 291      memoryId: null,
 292      namespace,
 293      purpose: taskHint ?? null,
 294      result: "ok",
 295      detail: `blocks=${selected.length} tokens=${totalTokens} budget=${budget} rejectedPins=${rejectedPins}${policyDetail}${brownoutDetail}`,
 296    } as const;
 297    // D36: with the kernel's transact/usage wiring the audit row and its
 298    // companion usage row commit in one transaction; module-level tests keep
 299    // the standalone append.
 300    const writeAudit = async (audit: AuditPort, tx?: unknown): Promise<void> => {
 301      const auditSeq = await audit.append(event);
 302      if (this.deps.usage && tx !== undefined && typeof auditSeq === "number" && args.usage) {
 303        await this.deps.usage(tx, {
 304          auditSeq,
 305          traceId: args.usage.traceId,
 306          ts: assembledAt,
 307          namespace,
 308          actor,
 309          principal: args.usage.principal,
 310          callerClass: args.usage.callerClass,
 311          op: "assemble",
 312          tool: args.usage.tool,
 313          result: "ok",
 314          latencyMs: performance.now() - t0,
 315          tokens: totalTokens,
 316          returned: selected.length,
 317          driver: null,
 318          sourceTier: null,
 319          sourceIds: null,
 320          topScore: null,
 321        });
 322      }
 323    };
 324    if (this.deps.transact) {
 325      await this.deps.transact(writeAudit);
 326    } else {
 327      await writeAudit(this.deps.audit);
 328    }
 329
 330    if (deliveredPolicy && this.deps.policyDirectives?.onDelivered) {
 331      try {
 332        await this.deps.policyDirectives.onDelivered(deliveredPolicy, args);
 333      } catch {
 334        // The ledger is evidence, not a gate: a failed compliance write never
 335        // costs the agent its working set.
 336      }
 337    }
 338
 339    return {
 340      namespace,
 341      blocks: selected,
 342      totalTokens,
 343      budget,
 344      assembledAt,
 345      ...(deliveredPolicy
 346        ? {
 347            policy: {
 348              digest: deliveredPolicy.digest,
 349              documents: deliveredPolicy.documents,
 350              tokens: deliveredPolicy.totalTokens,
 351              required: deliveredPolicy.required,
 352              truncated: deliveredPolicy.truncated,
 353            },
 354          }
 355        : {}),
 356    };
03 / brownout

Fault 命中必须过绝对分数门槛

源码判断

src/fault/index.ts:49 L2/L3 score floor 把 fault 变成有门槛的补召回。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/fault/index.ts:182 driver-planned L2 仍要经过 absoluteScores 校准。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/fault/index.ts:217 embedding brownout 会退到 BM25。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/fault/index.ts:265 L2 不足才走 L3 cold concept。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/fault/index.ts:317 access/fault telemetry 与 audit 一起提交。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/fault/index.ts:361 absoluteScores 的注释解释为什么 driver 分数不能直接和固定 floor 比。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

test/allocator-fault.test.ts:227 assemble 测试覆盖优先级与预算。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

test/allocator-fault.test.ts:459 embedding 失败测试确认 brownout 行为。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

test/allocator-fault.test.ts:586 driver-lane 测试防止只用返回 refs 计算 BM25 IDF。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

test/cold-encryption.test.ts:326 冷层 pin 组装测试确认 secret canary 不会进入错误 namespace 的 working set。 这处引用用于判断实现边界,而不是只给页面堆行号;它把设计说法落到可检查的控制流、数据模型或测试断言上,读者可以沿着同一文件继续追踪上下游。

src/fault/index.ts:160-249source
 160  async fault(args: FaultArgs): Promise<FaultResult> {
 161    const t0 = performance.now();
 162    const { namespace, query } = args;
 163    // Defensive: some callers (syscall layer) route audit attribution
 164    // themselves and may omit actor/agentId at runtime.
 165    const actor = args.actor ?? "unknown";
 166    const agentId = args.agentId ?? "unknown";
 167    const k = args.k ?? DEFAULT_K;
 168    const tokenBudget = args.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
 169
 170    // -- tier L2 --------------------------------------------------------------
 171    // M2: when the kernel wired a driver-routed planner, the L2 candidates
 172    // come from the policy-selected retriever cartridge; otherwise the
 173    // legacy hybrid search runs. Either way the ACCEPTANCE decision uses the
 174    // absolute hybrid score against L2_SCORE_FLOOR (see FaultHandlerDeps).
 175    let l2Refs: ScoredRef[];
 176    let acceptL2: boolean;
 177    let bumpTargetId: string | null;
 178    let driverDetail = "";
 179    let driverName: string | null = null;
 180    let degradedReason: FaultResult["degradedReason"];
 181
 182    try {
 183      if (this.deps.l2Planner) {
 184        const outcome = await this.deps.l2Planner(query, k, tokenBudget, args.access);
 185        driverDetail =
 186          ` driver=${outcome.driver}` +
 187          (outcome.fallback ? ` driver_fallback=${outcome.fallback}` : "");
 188        driverName = outcome.driver;
 189        l2Refs = outcome.refs;
 190        const abs = await this.absoluteScores(namespace, query, outcome.refs, args.access);
 191        acceptL2 = abs.some((s) => s >= L2_SCORE_FLOOR);
 192        bumpTargetId = outcome.refs[0]?.ref ?? null;
 193      } else {
 194        const l2Hits: HybridHit[] = await hybridSearchL2({
 195          index: this.deps.index,
 196          embedder: this.deps.embedder,
 197          namespace,
 198          query,
 199          k: Math.max(k * 3, 12),
 200          access: args.access,
 201        });
 202        const bestL2 = l2Hits.length > 0 ? l2Hits[0] : null;
 203        acceptL2 = bestL2 !== null && bestL2.score >= L2_SCORE_FLOOR;
 204        bumpTargetId = bestL2?.row.id ?? null;
 205        l2Refs = l2Hits
 206          .filter((h) => h.score > 0)
 207          .slice(0, k)
 208          .map((h) => ({
 209            id: h.row.id,
 210            tier: "L2" as const,
 211            ref: h.row.id,
 212            score: h.score,
 213            content: h.row.content,
 214            tokenEstimate: estimateTokens(h.row.content),
 215          }));
 216      }
 217    } catch {
 218      // Immediate brownout, including failures before the circuit breaker has
 219      // opened: retain the governed lexical lane and expose the degradation.
 220      const rows = await this.deps.index.listCandidates(namespace, {
 221        validOnly: true,
 222        limit: 200,
 223        access: args.access,
 224      });
 225      const scores = bm25Scores(
 226        query,
 227        rows.map((row) => ({ ref: row.id, content: row.content })),
 228      );
 229      l2Refs = rows
 230        .map((row) => ({
 231          id: row.id,
 232          tier: "L2" as const,
 233          ref: row.id,
 234          // In a lexical-only brownout the BM25 lane owns the full confidence
 235          // scale; retaining the hybrid 0.4 weight would make its maximum too
 236          // small to clear the calibrated acceptance floor reliably.
 237          score: saturate(scores.get(row.id) ?? 0),
 238          content: row.content,
 239          tokenEstimate: estimateTokens(row.content),
 240        }))
 241        .filter((ref) => ref.score > 0)
 242        .sort((a, b) => b.score - a.score || a.ref.localeCompare(b.ref))
 243        .slice(0, k);
 244      acceptL2 = (l2Refs[0]?.score ?? 0) >= L2_SCORE_FLOOR;
 245      bumpTargetId = l2Refs[0]?.ref ?? null;
 246      driverDetail = " driver=bm25-degraded brownout=embedding_unavailable";
 247      driverName = "bm25-degraded";
 248      degradedReason = "embedding_unavailable";
 249    }
src/fault/index.ts:260-358source
 260      result = "ok";
 261      if (injected.length > 0) {
 262        accessIds = injected.map((r) => r.ref);
 263      }
 264    } else {
 265      // -- tier L3: cold concept search --------------------------------------
 266      let l3Refs: ScoredRef[];
 267      try {
 268        l3Refs = (
 269          await this.deps.cold.searchConcepts(
 270            this.deps.db,
 271            this.deps.embedder,
 272            namespace,
 273            query,
 274            k,
 275          )
 276        ).filter((s) => s.score > L3_SCORE_FLOOR);
 277      } catch {
 278        // Brownout: an unavailable/corrupt cold tier must not crash the agent
 279        // after the governed L2 lane has already been exhausted. The additive
 280        // result field and audit detail distinguish this from a genuine miss.
 281        l3Refs = [];
 282        degradedReason = "cold_unavailable";
 283      }
 284
 285      if (l3Refs.length > 0) {
 286        source = "L3";
 287        injected = packToBudget(l3Refs.slice(0, k), tokenBudget);
 288        result = "ok";
 289      } else {
 290        source = null;
 291        injected = [];
 292        result = "miss";
 293      }
 294      // Fault pressure: the closest L2 memory was not good enough.
 295      if (bumpTargetId !== null) {
 296        faultId = bumpTargetId;
 297      }
 298    }
 299
 300    const latencyMs = Math.max(0, Math.round(performance.now() - t0));
 301    const injectedTokens = injected.reduce((s, r) => s + r.tokenEstimate, 0);
 302
 303    const event = {
 304      ts: new Date().toISOString(),
 305      actor,
 306      agentId,
 307      op: "fault",
 308      memoryId: injected.length > 0 ? injected[0].id : null,
 309      namespace,
 310      purpose: args.auditPurpose ?? query,
 311      result,
 312      detail:
 313        `source=${source ?? "none"} injected=${injected.length} tokens=${injectedTokens} latencyMs=${latencyMs}${driverDetail}` +
 314        (degradedReason ? ` brownout=${degradedReason}` : ""),
 315    } as const;
 316
 317    const mutateAndAudit = async (index: L2IndexPort, audit: AuditPort, tx?: TDb): Promise<void> => {
 318      if (accessIds.length > 0) await index.bumpAccess(accessIds);
 319      if (faultId !== null) await index.bumpFault(faultId);
 320      const auditSeq = await audit.append(event);
 321      // D36: the companion usage row rides the audit transaction (the hook
 322      // wraps its writes in a SAVEPOINT brownout). The audit detail string
 323      // above stays byte-identical.
 324      if (this.deps.usage && tx !== undefined && typeof auditSeq === "number" && args.usage) {
 325        await this.deps.usage(tx, {
 326          auditSeq,
 327          traceId: args.usage.traceId,
 328          ts: event.ts,
 329          namespace,
 330          actor,
 331          principal: args.usage.principal,
 332          callerClass: args.usage.callerClass,
 333          op: "fault",
 334          tool: args.usage.tool,
 335          result,
 336          latencyMs,
 337          tokens: injectedTokens,
 338          returned: injected.length,
 339          driver: driverName,
 340          sourceTier: source === "L2" ? "warm" : source === "L3" ? "cold" : null,
 341          sourceIds: null,
 342          topScore: injected[0]?.score ?? null,
 343        });
 344      }
 345    };
 346    if (this.deps.transact) {
 347      await this.deps.transact(mutateAndAudit);
 348    } else {
 349      await mutateAndAudit(this.deps.index, this.deps.audit);
 350    }
 351
 352    return {
 353      hit: result === "ok",
 354      source,
 355      injected,
 356      latencyMs,
 357      ...(degradedReason ? { degradedReason } : {}),
 358    };
src/fault/index.ts:361-403source
 361  /**
 362   * Absolute 0.6·cosine + 0.4·saturated-BM25 score per driver-planned ref —
 363   * the same scale L2_SCORE_FLOOR was calibrated against (driver plan()
 364   * scores are per-query normalized and NOT comparable to a fixed floor).
 365   *
 366   * BM25 IDF is computed over the SAME ~200-row ACL-scoped candidate pool
 367   * the legacy hybridSearchL2 lane used, NOT over just the <=k returned refs.
 368   * With an n<=k pool the IDF of a rare query term collapses (≈log(1.2)
 369   * instead of ≈log(pool/df)), systematically deflating lexical-dominant
 370   * matches below a floor they cleared in M1 — a silent fault hit-rate
 371   * regression. Refs beyond the pool cap are unioned in so every ref still
 372   * gets a score.
 373   */
 374  private async absoluteScores(
 375    namespace: Namespace,
 376    query: string,
 377    refs: ScoredRef[],
 378    access?: AccessSubject,
 379  ): Promise<number[]> {
 380    if (refs.length === 0) return [];
 381    const [queryVec] = await this.deps.embedder.embed([query]);
 382    const contentVecs = await this.deps.embedder.embed(refs.map((r) => r.content));
 383
 384    // IDF context: full ACL-scoped candidate pool (M1 parity: hybridSearchL2
 385    // ran bm25Scores over listCandidates(limit 200) ∪ vectorSearch).
 386    const pool = await this.deps.index.listCandidates(namespace, {
 387      validOnly: true,
 388      limit: 200,
 389      access,
 390    });
 391    const docsByRef = new Map<string, { ref: string; content: string }>();
 392    for (const row of pool) docsByRef.set(row.id, { ref: row.id, content: row.content });
 393    for (const r of refs) {
 394      if (!docsByRef.has(r.ref)) docsByRef.set(r.ref, { ref: r.ref, content: r.content });
 395    }
 396    const bm = bm25Scores(query, [...docsByRef.values()]);
 397
 398    return refs.map((r, i) => {
 399      const vecScore = Math.max(0, cosineSim(queryVec, contentVecs[i]));
 400      const bmScore = saturate(bm.get(r.ref) ?? 0);
 401      return 0.6 * vecScore + 0.4 * bmScore;
 402    });
 403  }