Files
brain/tools/decomposition-detector.mjs
T

65 lines
2.6 KiB
JavaScript
Raw Normal View History

// tools/decomposition-detector.mjs
/**
* Decomposition detector — router-gate v4 spec §3.8 + v4.1 (Direction 3).
* Pure: ловит feature, разбитую на 3+ мелких prompts с overlapping keywords без plan skill.
* v4.1: hard-block mutating at 3+ overlapping (was 5+ soft). LLM-judge verdict инъектируется.
*/
import { keywordOverlapCount, isResetMarker } from './safe-baseline-metering.mjs';
export { isResetMarker };
export const V4_1_DECOMP_THRESHOLD = Object.freeze({
min_overlapping_prompts: 3,
min_keyword_intersection: 3,
window_size_prompts: 10,
hard_block_mutating: true,
});
export function keywordIntersection(a, b) {
return keywordOverlapCount(a, b);
}
export function appendHistory(history, entry) {
return [...(history || []), entry];
}
export function detectDecompositionCandidate(history, currentEntry, threshold = V4_1_DECOMP_THRESHOLD) {
const window = (history || []).slice(-threshold.window_size_prompts);
const curKws = currentEntry.primary_keywords || [];
const overlapping = window.filter(
(e) => keywordOverlapCount(e.primary_keywords || [], curKws) >= threshold.min_keyword_intersection,
);
const anySkill = [...overlapping, currentEntry].some((e) => e.skill_invoked_this_prompt === true);
if (overlapping.length >= threshold.min_overlapping_prompts && !anySkill) {
// overlappingKeywords: curKws present in EVERY overlapping prompt
const overlappingKeywords = curKws.filter((k) =>
overlapping.every(
(e) => (e.primary_keywords || []).map((x) => String(x).toLowerCase()).includes(String(k).toLowerCase()),
),
);
return {
candidate: true,
overlappingPrompts: overlapping.map((e) => e.prompt_idx),
overlappingKeywords,
reason: `${overlapping.length + 1} prompts overlapping keywords [${overlappingKeywords.join(', ')}] без writing-plans/brainstorming skill.`,
};
}
return { candidate: false, overlappingPrompts: [], overlappingKeywords: [] };
}
export function decideDecomposition(candidate, llmVerdict, threshold = V4_1_DECOMP_THRESHOLD) {
if (!candidate || !candidate.candidate) return { action: 'allow' };
const verdict = typeof llmVerdict === 'string' ? llmVerdict : llmVerdict?.verdict;
if (verdict === 'YES') {
return {
action: threshold.hard_block_mutating ? 'hard_block_mutating' : 'soft_flag',
reason: `v4.1 decomp hard-block: ${candidate.reason} LLM-judge confirmed decomposition. Invoke writing-plans skill сейчас.`,
};
}
// candidate but LLM says legit-distinct → soft surface only
return { action: 'soft_flag', reason: candidate.reason };
}