Files
rea/src/application/native/NativeMatchingAdmission.ts
T
b8ce3c74cd feat(native): make function matching allowance configurable (#7159)
Allow native matching representation budgets to be selected at runtime or launch while retaining the 96 MiB default and main shared-provenance accounting. Preserve actionable configuration recovery through the setting owner and verify complete CLI/MCP matches for 20k/50k synthetic inventories and real stripped LLVM 14/15 inventories.

Fixes #6406.

Based on #7112 by Kaoru0822; original authorship and co-author credit retained.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:22:32 +08:00

99 lines
3.5 KiB
TypeScript

import { setImmediate } from "node:timers/promises";
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
import type { Evidence } from "../../domain/evidence.js";
import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js";
import {
DEFAULT_NATIVE_MATCHING_ALLOWANCE,
nativeMatchingAllowanceRecovery,
nativeMatchingByteAllowance,
type NativeMatchingAllowance,
} from "../../config/nativeMatchingAllowance.js";
/** Budget the actual selected observations, before cloning or index allocation. */
export class NativeMatchingAdmission {
readonly #maximum: number;
readonly #source: NativeMatchingAllowance["source"];
#remaining: number;
#work = 0;
readonly #seen = new WeakSet<object>();
constructor(
readonly operation: string,
readonly signal?: AbortSignal,
allowance: NativeMatchingAllowance = DEFAULT_NATIVE_MATCHING_ALLOWANCE,
) {
// Read the accepted operation's value once, so the refusal reports the
// budget this request actually ran under.
this.#maximum = allowance.maximumBytes;
this.#source = allowance.source;
this.#remaining = this.#maximum;
}
readonly checkpoint = async (bytes = 0): Promise<void> => {
this.#remaining -= bytes;
if (this.#remaining < 0)
throw new AnalysisResourceConstraintError(
this.operation,
"capacity",
`Native function matching exceeds its ${nativeMatchingByteAllowance(this.#maximum)} representation allowance (native.matching_representation_bytes).`,
{ maximum_bytes: this.#maximum },
{
retryable: false,
remediationAction: nativeMatchingAllowanceRecovery({
maximumBytes: this.#maximum,
source: this.#source,
}),
limit: analysisSettingLimit(
"native.matching_representation_bytes",
this.#maximum,
this.#source,
),
},
);
this.signal?.throwIfAborted();
if (++this.#work % 256 === 0)
await setImmediate(
undefined,
this.signal === undefined ? {} : { signal: this.signal },
);
};
/**
* Charge a borrowed record's envelope, which matching copies into its
* limitations, digests and links. Its result payloads stay resident: the
* caller charges what matching derives from them, or never parses them.
*/
async admitBorrowed(record: Evidence): Promise<void> {
await this.checkpoint(128);
for (const [key, value] of Object.entries(record))
if (key !== "raw_result" && key !== "normalized_result")
await this.admit(value);
}
async admit(root: unknown): Promise<void> {
const pending: Iterator<unknown>[] = [[root][Symbol.iterator]()];
while (pending.length) {
const next = pending[pending.length - 1]!.next();
if (next.done) {
pending.pop();
continue;
}
const value = next.value;
if (typeof value === "object" && value !== null) {
if (this.#seen.has(value)) continue;
this.#seen.add(value);
await this.checkpoint(128);
// Iterator traversal avoids a second width-sized array of child values.
pending.push(
(function* () {
for (const key in value)
if (Object.hasOwn(value, key)) {
yield key;
yield (value as Record<string, unknown>)[key];
}
})(),
);
} else
await this.checkpoint(
typeof value === "string" ? 64 + value.length * 4 : 128,
);
}
}
}