65 lines
2.6 KiB
JavaScript
65 lines
2.6 KiB
JavaScript
|
|
// 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 };
|
|||
|
|
}
|