src / detector.ts
/**
* Stream repetition detector based on the Z-function reversed (prefix matching).
*
* Works the entire accumulated normalized text (bounded by maxTextLength).
*
* How it works:
* - We keep a buffer of the last `maxTextLength` normalized characters.
* Every `checkInterval` characters we compute the Z-function for this buffer.
* - Z[i] = length of the longest common prefix between the text and the text
* starting at i.
* - If max(Z[i]) >= zThreshold (derived from the old `threshold` param), we
* consider it a loop.
* - zThreshold = threshold - 1 (e.g., old threshold=10 -> zThreshold=9). This
* matches the old behavior for unit-length-1 repetitions: a run of 10
* identical chars -> max Z[i] = 9 -> trigger.
* - For longer units, the reaction is earlier (more sensitive), which is
* desirable.
* - We approximate the repetition count as floor(maxZ / zThreshold) + 1
* (e.g., maxZ=18, zThreshold=9 -> count=3).
*
*/
const fs = require('fs');
const path = require('path');
export interface DetectorConfig {
/** Threshold for consecutive-unit detection (now used to derive zThreshold). */
threshold: number;
/** How often (in normalized chars) to run the Z-function check. */
checkInterval: number;
/** Maximum length of normalized text to keep for the Z-function check. */
maxTextLength: number;
/** Characters stripped before matching (Markdown table decorations). Default "-|". */
ignoredChars?: string;
}
export interface RepetitionHit {
kind: "z-function";
/** The matched prefix (normalized, may be truncated for display). */
unit: string;
/** Approximate repetition count based on max Z and zThreshold. */
count: number;
/** The threshold that was met. */
threshold: number;
/** Index i where the suffix matches the buffer's prefix (start of the loop region). */
distance: number;
/** match Length **/
matchLen: number;
}
export function defaultDetectorConfig(): DetectorConfig {
return {
threshold: 10,
checkInterval: 32,
maxTextLength: 20000,
ignoredChars: "-|",
};
}
export function output(n_match:number, n_loop:number, n_calc:number, mult:number){
// НАЧАЛО БЛОКА ЛОГИРОВАНИЯ
// ==========================================
try {
// Путь к файлу лога (сохранится в папку плагина под именем buffer_log.txt)
const logPath = path.join(__dirname, 'buffer.log');
// Формируем красивую запись с текущим временем и длиной буфера
const timestamp = new Date().toISOString();
const logMessage = `--- [${timestamp}]\nloop: ${n_loop} симв. Multiplier: ${mult}` +
` match: ${n_match} симв. Calculated Treshold: ${n_calc}\n` +
// `ИНВЕРТИРОВАННЫЙ ТЕКСТ ДЛЯ Z-ФУНКЦИИ:\n${reversedText}\n` +
`--------------------------------------------------\n\n`;
// Записываем в файл (appendFileSync дописывает в конец файла, не стирая старое)
fs.appendFileSync(logPath, logMessage, 'utf8');
} catch (logError) {
console.error("Ошибка записи в лог-файл detector buffer.log :", logError);
}
// ==========================================
// КОНЕЦ БЛОКА ЛОГИРОВАНИЯ
}
/**
* Stream repetition detector using the Z-function.
*/
export class StreamRepetitionDetector {
private _repetitionThreshold: number;
private _checkInterval: number;
private _maxTextLength: number;
private _lastCheckPos: number = 0;
private _text: string = "";
private cfg: any;
constructor(cfg?: Partial<DetectorConfig>) {
// Задаем дефолтные значения на случай, если cfg пустой
const defaultCfg = {
...defaultDetectorConfig(),
repetitionThreshold: 1.5, // 1.5 раза по умолчанию (150%)
checkInterval: 1,
maxTextLength: 20000
};
const merged = { ...defaultCfg, ...cfg };
// Проверки безопасности для интервалов и длины буфера
if (merged.checkInterval < 1) throw new Error("checkInterval must be at least 1");
if (merged.maxTextLength < 1) throw new Error("maxTextLength must be at least 2000");
// Валидация множителя повторений (от 0.25 до 4.0)
const thresh = merged.repetitionThreshold ?? 1.5;
if (thresh < 0.25 || thresh > 4.0) throw new Error("repetitionThreshold must be between 0.25 and 4.0");
this.cfg = merged;
this._checkInterval = merged.checkInterval;
this._maxTextLength = merged.maxTextLength;
// Сохраняем коэффициент повторений в приватное свойство класса
this._repetitionThreshold = thresh;
}
/** Normalize: remove whitespace, ignore `ignoredChars`, lowercase. */
private normalize(fragment: string): string {
const ignored = this.cfg.ignoredChars ?? "-|";
let out = "";
for (const ch of fragment) {
if (/\s/.test(ch)) continue;
if (ignored.includes(ch)) continue;
out += ch.toLowerCase();
}
return out;
}
// new push() -- dinamically get percent from config - checkAndStop() in generator
push = (fragment: string, currentMultiplier?: number) => { // push(fragment: string): RepetitionHit | null {
const norm = this.normalize(fragment);
if (norm.length === 0) return null;
this._text += norm;
if (this._text.length > this._maxTextLength) {
this._text = this._text.slice(this._text.length - this._maxTextLength);
}
const currentLength = this._text.length;
if (currentLength - this._lastCheckPos < this._checkInterval) {
return null;
}
this._lastCheckPos = currentLength;
const n = this._text.length;
if (n < 2) return null;
const reversedText = this._text.split("").reverse().join("");
let maxZ = 0;
let maxZIndex = 0;
let l = 0;
let r = 0;
const z = new Array(n).fill(0);
for (let i = 1; i < n; i++) {
if (i <= r) {
z[i] = Math.min(r - i + 1, z[i - l]);
}
while (
i + z[i] < n &&
reversedText.charAt(z[i]) === reversedText.charAt(i + z[i])
) {
z[i]++;
}
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
if (z[i] > maxZ) {
maxZ = z[i];
maxZIndex = i; // maxZIndex — это шаг петли (distance)
}
}
const distance = maxZIndex;
const matchLen = maxZ;
// Внутренний множитель (дефолт 1.5, если что-то пошло не так)
//const multiplier = (this._repetitionThreshold && !isNaN(this._repetitionThreshold))
// ? this._repetitionThreshold
// : 1.5;
const multiplier = (currentMultiplier && !isNaN(currentMultiplier))
? currentMultiplier
: 1.5;
// ВЫЧИСЛЕНИЕ ПОРОГА ПО coeff
// Умножаем шаг петли на коэффициент. Минимум 8 символов защиты.
// const calculatedThreshold = Math.max(16, Math.floor(distance * multiplier));
// if(matchLen>20){
// output(matchLen,distance,calculatedThreshold,multiplier); //DEBUG Multiplier: 1.5
// }
// Условие триггера: накопленное совпадение пробило кратный порог
// Вычисляем динамический порог
const calculatedThreshold = Math.max(16, Math.floor(distance * multiplier) | 0);
if (matchLen >= calculatedThreshold && distance > 10) {
const rawUnit = reversedText.substring(0, Math.min(40, distance));
const unit = rawUnit.split("").reverse().join("");
const exactCount = parseFloat((matchLen / distance).toFixed(2));
// Точный подсчет: сколько полных и дробных раз уложился цикл в совпадении
return {
kind: "z-function-multiplier",
unit,
count: exactCount, // Выводим реальную кратность (например, 1.54 или 3.1)
threshold: calculatedThreshold, // Порог в символах для текущей дистанции
distance, // Длина самой петли
matchLen, // Полная длина совпадения символов
};
}
return null;
}
/** Reset all state (e.g. between predictions). */
reset(): void {
this._text = "";
this._lastCheckPos = 0;
}
}
src / detector.ts
/**
* Stream repetition detector based on the Z-function reversed (prefix matching).
*
* Works the entire accumulated normalized text (bounded by maxTextLength).
*
* How it works:
* - We keep a buffer of the last `maxTextLength` normalized characters.
* Every `checkInterval` characters we compute the Z-function for this buffer.
* - Z[i] = length of the longest common prefix between the text and the text
* starting at i.
* - If max(Z[i]) >= zThreshold (derived from the old `threshold` param), we
* consider it a loop.
* - zThreshold = threshold - 1 (e.g., old threshold=10 -> zThreshold=9). This
* matches the old behavior for unit-length-1 repetitions: a run of 10
* identical chars -> max Z[i] = 9 -> trigger.
* - For longer units, the reaction is earlier (more sensitive), which is
* desirable.
* - We approximate the repetition count as floor(maxZ / zThreshold) + 1
* (e.g., maxZ=18, zThreshold=9 -> count=3).
*
*/
const fs = require('fs');
const path = require('path');
export interface DetectorConfig {
/** Threshold for consecutive-unit detection (now used to derive zThreshold). */
threshold: number;
/** How often (in normalized chars) to run the Z-function check. */
checkInterval: number;
/** Maximum length of normalized text to keep for the Z-function check. */
maxTextLength: number;
/** Characters stripped before matching (Markdown table decorations). Default "-|". */
ignoredChars?: string;
}
export interface RepetitionHit {
kind: "z-function";
/** The matched prefix (normalized, may be truncated for display). */
unit: string;
/** Approximate repetition count based on max Z and zThreshold. */
count: number;
/** The threshold that was met. */
threshold: number;
/** Index i where the suffix matches the buffer's prefix (start of the loop region). */
distance: number;
/** match Length **/
matchLen: number;
}
export function defaultDetectorConfig(): DetectorConfig {
return {
threshold: 10,
checkInterval: 32,
maxTextLength: 20000,
ignoredChars: "-|",
};
}
export function output(n_match:number, n_loop:number, n_calc:number, mult:number){
// НАЧАЛО БЛОКА ЛОГИРОВАНИЯ
// ==========================================
try {
// Путь к файлу лога (сохранится в папку плагина под именем buffer_log.txt)
const logPath = path.join(__dirname, 'buffer.log');
// Формируем красивую запись с текущим временем и длиной буфера
const timestamp = new Date().toISOString();
const logMessage = `--- [${timestamp}]\nloop: ${n_loop} симв. Multiplier: ${mult}` +
` match: ${n_match} симв. Calculated Treshold: ${n_calc}\n` +
// `ИНВЕРТИРОВАННЫЙ ТЕКСТ ДЛЯ Z-ФУНКЦИИ:\n${reversedText}\n` +
`--------------------------------------------------\n\n`;
// Записываем в файл (appendFileSync дописывает в конец файла, не стирая старое)
fs.appendFileSync(logPath, logMessage, 'utf8');
} catch (logError) {
console.error("Ошибка записи в лог-файл detector buffer.log :", logError);
}
// ==========================================
// КОНЕЦ БЛОКА ЛОГИРОВАНИЯ
}
/**
* Stream repetition detector using the Z-function.
*/
export class StreamRepetitionDetector {
private _repetitionThreshold: number;
private _checkInterval: number;
private _maxTextLength: number;
private _lastCheckPos: number = 0;
private _text: string = "";
private cfg: any;
constructor(cfg?: Partial<DetectorConfig>) {
// Задаем дефолтные значения на случай, если cfg пустой
const defaultCfg = {
...defaultDetectorConfig(),
repetitionThreshold: 1.5, // 1.5 раза по умолчанию (150%)
checkInterval: 1,
maxTextLength: 20000
};
const merged = { ...defaultCfg, ...cfg };
// Проверки безопасности для интервалов и длины буфера
if (merged.checkInterval < 1) throw new Error("checkInterval must be at least 1");
if (merged.maxTextLength < 1) throw new Error("maxTextLength must be at least 2000");
// Валидация множителя повторений (от 0.25 до 4.0)
const thresh = merged.repetitionThreshold ?? 1.5;
if (thresh < 0.25 || thresh > 4.0) throw new Error("repetitionThreshold must be between 0.25 and 4.0");
this.cfg = merged;
this._checkInterval = merged.checkInterval;
this._maxTextLength = merged.maxTextLength;
// Сохраняем коэффициент повторений в приватное свойство класса
this._repetitionThreshold = thresh;
}
/** Normalize: remove whitespace, ignore `ignoredChars`, lowercase. */
private normalize(fragment: string): string {
const ignored = this.cfg.ignoredChars ?? "-|";
let out = "";
for (const ch of fragment) {
if (/\s/.test(ch)) continue;
if (ignored.includes(ch)) continue;
out += ch.toLowerCase();
}
return out;
}
// new push() -- dinamically get percent from config - checkAndStop() in generator
push = (fragment: string, currentMultiplier?: number) => { // push(fragment: string): RepetitionHit | null {
const norm = this.normalize(fragment);
if (norm.length === 0) return null;
this._text += norm;
if (this._text.length > this._maxTextLength) {
this._text = this._text.slice(this._text.length - this._maxTextLength);
}
const currentLength = this._text.length;
if (currentLength - this._lastCheckPos < this._checkInterval) {
return null;
}
this._lastCheckPos = currentLength;
const n = this._text.length;
if (n < 2) return null;
const reversedText = this._text.split("").reverse().join("");
let maxZ = 0;
let maxZIndex = 0;
let l = 0;
let r = 0;
const z = new Array(n).fill(0);
for (let i = 1; i < n; i++) {
if (i <= r) {
z[i] = Math.min(r - i + 1, z[i - l]);
}
while (
i + z[i] < n &&
reversedText.charAt(z[i]) === reversedText.charAt(i + z[i])
) {
z[i]++;
}
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
if (z[i] > maxZ) {
maxZ = z[i];
maxZIndex = i; // maxZIndex — это шаг петли (distance)
}
}
const distance = maxZIndex;
const matchLen = maxZ;
// Внутренний множитель (дефолт 1.5, если что-то пошло не так)
//const multiplier = (this._repetitionThreshold && !isNaN(this._repetitionThreshold))
// ? this._repetitionThreshold
// : 1.5;
const multiplier = (currentMultiplier && !isNaN(currentMultiplier))
? currentMultiplier
: 1.5;
// ВЫЧИСЛЕНИЕ ПОРОГА ПО coeff
// Умножаем шаг петли на коэффициент. Минимум 8 символов защиты.
// const calculatedThreshold = Math.max(16, Math.floor(distance * multiplier));
// if(matchLen>20){
// output(matchLen,distance,calculatedThreshold,multiplier); //DEBUG Multiplier: 1.5
// }
// Условие триггера: накопленное совпадение пробило кратный порог
// Вычисляем динамический порог
const calculatedThreshold = Math.max(16, Math.floor(distance * multiplier) | 0);
if (matchLen >= calculatedThreshold && distance > 10) {
const rawUnit = reversedText.substring(0, Math.min(40, distance));
const unit = rawUnit.split("").reverse().join("");
const exactCount = parseFloat((matchLen / distance).toFixed(2));
// Точный подсчет: сколько полных и дробных раз уложился цикл в совпадении
return {
kind: "z-function-multiplier",
unit,
count: exactCount, // Выводим реальную кратность (например, 1.54 или 3.1)
threshold: calculatedThreshold, // Порог в символах для текущей дистанции
distance, // Длина самой петли
matchLen, // Полная длина совпадения символов
};
}
return null;
}
/** Reset all state (e.g. between predictions). */
reset(): void {
this._text = "";
this._lastCheckPos = 0;
}
}