step 1: calling local:qwen3.8:27b
tool shell <- {"command":"npm run --silent test 2>&1 | tail -50"}
tool shell ok: exit code: 0
stdout:
PASS tests/unit/generators/utils/parse.tests.ts
PASS tests/unit/utils/state-manager.tests.ts
PASS tests/unit/utils/url.tests.ts
FAIL tests/unit/config/config.tests.ts
  ● Dynamic Theme Fixes config

    TypeError: (invert || []).concat(...).concat(...).every is not a function

      167 |     // selectors should have no comma
      168 |     const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    > 169 |     expect(fixes.every(({invert, ignoreInlineStyle, ignoreImageAnalysis}) => (invert || []).concat(ignoreInlineStyle || []).concat(ignoreImageAnalysis || []).every((s) => !commaSelector.test(s)))).toBe(true);
          |                                                                                                                                                               ^
      170 |
      171 |     // fixes are properly formatted
      172 |     expect(throwIfDifferent(file, formatDynamicThemeFixes(fixes), 'Dynamic fixes format error')).not.toThrow();

      at tests/unit/config/config.tests.ts:169:159
          at Array.every (<anonymous>)
      at Object.<anonymous> (tests/unit/config/config.tests.ts:169:18)

PASS tests/unit/utils/time.tests.ts
PASS tests/unit/inject/fixes.tests.ts
PASS tests/unit/utils/color.tests.ts
PASS tests/unit/utils/parsing.tests.ts
PASS tests/unit/utils/validation.tests.ts
PASS tests/unit/utils/promise-barrier.tests.ts
PASS tests/unit/utils/text.tests.ts
PASS tests/unit/inject/selectors.tests.ts
PASS tests/unit/config/locales.tests.ts
PASS tests/unit/utils/uid.tests.ts
PASS tests/unit/utils/math.tests.ts
PASS tests/unit/csp/csp.tests.ts

Test Suites: 1 failed, 15 passed, 16 total
Tests:       1 failed, 74 passed, 75 total
Snapshots:   0 total
Time:        2.281 s
Ran all test suites.
[evidence record sha256:12cbe2c488582a3ee93cc94822cbd55e13a23849e321f5be81d88b194f9ac7a7 kind tool-call:shell]
tool list <- {}
tool list ok: .git/
.gitattributes
.github/
.gitignore
.npmignore
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
README.md
SECURITY.md
docs/
eslint-plugin-local.js
eslint.config.js
index.d.ts
integrity/
node_modules/
package-lock.json
package.json
src/
tasks/
tests/
[evidence record sha256:4c00fe9571a615211102b3d191962f7d2bf872f90c7a2554edfe3e0ba5d8c740 kind tool-call:list]
step 2: calling local:qwen3.8:27b
tool read <- {"path":"tests/unit/config/config.tests.ts"}
tool read ok: import {readFile} from 'node:fs';

import type {StaticTheme} from '../../../src/definitions';
import {parseInversionFixes, formatInversionFixes} from '../../../src/generators/css-filter';
import {parseDetectorHints, formatDetectorHints} from '../../../src/generators/detector-hints';
import {parseDynamicThemeFixes, formatDynamicThemeFixes} from '../../../src/generators/dynamic-theme';
import {parseStaticThemes, formatStaticThemes} from '../../../src/generators/static-theme';
import {parseColorSchemeConfig} from '../../../src/utils/colorscheme-parser';
import type {ParsedColorSchemeConfig} from '../../../src/utils/colorscheme-parser';
import {parseArray, formatArray, getTextDiffIndex, getTextPositionMessage} from '../../../src/utils/text';
import {compareURLPatterns} from '../../../src/utils/url';
import {rootPath} from '../../support/test-utils';

function readConfig(fileName: string) {
    return new Promise<string>((resolve, reject) => {
        readFile(rootPath('src/config', fileName), {encoding: 'utf-8'}, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            resolve(data);
        });
    });
}

function isURLPatternValid(url: string) {
    return url.length > 0 && url.indexOf('://') < 0;
}

function throwIfDifferent(input: string, expected: string, message: string) {
    return () => {
        const diffIndex = getTextDiffIndex(input, expected);
        if (diffIndex >= 0) {
            throw new Error(`${message}\n${getTextPositionMessage(input, diffIndex)}`);
        }
    };
}

function formatColorSchemeConfig(scheme: ParsedColorSchemeConfig): string {
    const names = Object.keys(scheme.dark);
    const lines = [];
    for (const name of names) {
        lines.push(name);
        lines.push('');
        for (const color of ['dark', 'light']) {
            const style = scheme[color as keyof ParsedColorSchemeConfig][name];
            if (style) {
                const {backgroundColor, textColor} = style;
                lines.push(color.toUpperCase());
                if (backgroundColor) {
                    lines.push(`background: ${backgroundColor.toLowerCase()}`);
                }
                if (textColor) {
                    lines.push(`text: ${textColor.toLowerCase()}`);
                }
                lines.push('');
            }
        }
        lines.push('='.repeat(32));
        lines.push('');
    }
    lines.pop();
    lines.pop();
    return lines.join('\n');
}

test('Dark Sites list', async () => {
    const file = await readConfig('dark-sites.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const sites = parseArray(file);

    // is not empty
    expect(sites.length).toBeGreaterThan(0);

    // url patterns should have no protocol
    expect(sites.every(isURLPatternValid)).toBe(true);

    // sites are sorted alphabetically
    expect(sites.slice().sort(compareURLPatterns)).toEqual(sites);

    // sites are properly formatted
    expect(throwIfDifferent(file, formatArray(sites), 'Dark Sites list format error')).not.toThrow();
});

test('Detector Hints config', async () => {
    const file = await readConfig('detector-hints.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const hints = parseDetectorHints(file);

    // each hint has valid URL
    expect(hints.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // hints are sorted alphabetically
    expect(hints.map(({url}) => url[0])).toEqual(hints.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    expect(hints.every(({target, match}) => ![target].concat(match).some((s) => commaSelector.test(s)))).toBe(true);

    // only a single selector is allowed for target
    expect(hints.every(({target, noDarkTheme, systemTheme}) => noDarkTheme || systemTheme || typeof target === 'string' && !target.includes('\n'))).toBe(true);

    // hints are properly formatted
    expect(throwIfDifferent(file, formatDetectorHints(hints), 'Detector Hints format error')).not.toThrow();

    // should parse empty config
    expect(parseDetectorHints('')).toEqual([]);

    // should skip unsupported commands
    expect(parseDetectorHints([
        'inbox.google.com',
        'mail.google.com',
        'TARGET', 'a',
        'MATCH', '.b', '#c',
        'UNSUPPORTED', 'c',
        '========',
        'proton.me',
        'SYSTEM THEME',
        '========',
        'twitter.com',
        'UNSUPPORTED', 'a', 'b',
        'TARGET', 'c',
        'MATCH', '[d="e"]',
        '========',
        'wikipedia.org',
        'NO DARK THEME',
    ].join('\n'))).toEqual([
        {url: ['inbox.google.com', 'mail.google.com'], target: 'a', match: ['.b', '#c']},
        {url: ['proton.me'], systemTheme: true},
        {url: ['twitter.com'], target: 'c', match: ['[d="e"]']},
        {url: ['wikipedia.org'], noDarkTheme: true},
    ] as any);
});

test('Dynamic Theme Fixes config', async () => {
    const file = await readConfig('dynamic-theme-fixes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const fixes = parseDynamicThemeFixes(file);

    // there is a common fix
    expect(fixes[0].url[0]).toEqual('*');

    // each fix has valid URL
    expect(fixes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // fixes are sorted alphabetically
    expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    expect(fixes.every(({invert, ignoreInlineStyle, ignoreImageAnalysis}) => (invert || []).concat(ignoreInlineStyle || []).concat(ignoreImageAnalysis || []).every((s) => !commaSelector.test(s)))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatDynamicThemeFixes(fixes), 'Dynamic fixes format error')).not.toThrow();

    // should parse empty config
    expect(parseDynamicThemeFixes('')).toEqual([]);

    // should skip unsupported commands
    expect(parseDynamicThemeFixes([
        'inbox.google.com',
        'mail.google.com',
        'INVERT', 'a', 'b',
        'CSS', '.x { color: white !important; }',
        'UNSUPPORTED', 'c', 'd',
        '========',
        'twitter.com',
        'UNSUPPORTED', 'a', 'b',
        'INVERT', 'c', 'd',
        '========',
        'wikipedia.org',
        'IGNORE INLINE STYLE', 'a', 'b',
        '========',
        'duckduckgo.com',
        'IGNORE IMAGE ANALYSIS', 'img[alt="Logo"]', 'canvas',
    ].join('\n'))).toEqual([
        {url: ['inbox.google.com', 'mail.google.com'], invert: ['a', 'b'], css: '.x { color: white !important; }'},
        {url: ['twitter.com'], invert: ['c', 'd']},
        {url: ['wikipedia.org'], ignoreInlineStyle: ['a', 'b']},
        {url: ['duckduckgo.com'], ignoreImageAnalysis: ['img[alt="Logo"]', 'canvas']},
    ] as any);
});

test('Inversion Fixes config', async () => {
    const file = await readConfig('inversion-fixes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const fixes = parseInversionFixes(file);

    // there is a common fix
    expect(fixes[0].url[0]).toEqual('*');

    // each fix has valid URL
    expect(fixes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // fixes are sorted alphabetically
    expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    expect(fixes.every(({invert, noinvert, removebg}) => (invert || []).concat(noinvert || []).concat(removebg || []).every((s) => s.indexOf(',') < 0))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatInversionFixes(fixes), 'Inversion fixes format error')).not.toThrow();
});

test('Static Themes config', async () => {
    const file = await readConfig('static-themes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const themes = parseStaticThemes(file);

    // there is a common theme
    expect(themes[0].url[0]).toEqual('*');

    // each theme has valid URL
    expect(themes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // themes are sorted alphabetically
    expect(themes.map(({url}) => url[0])).toEqual(themes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    expect(themes.every((t) => (Object.keys(t) as Array<keyof StaticTheme>)
        .filter((prop) => ['url', 'noCommon'].indexOf(prop) < 0)
        .every((prop) => (t[prop] as string[])
            .every((s) => s.indexOf(',') < 0)))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatStaticThemes(themes), 'Static theme format error')).not.toThrow();
});

test('Colorscheme config', async () => {
    const file = await readConfig('color-schemes.drconf');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const {result: schemes, error} = parseColorSchemeConfig(file);

    // Their is no error
    expect(error).toBeNull();

    // There is a default Dark color scheme
    expect(schemes.dark['Default']).toBeDefined();

    // There is a default Light color scheme
    expect(schemes.light['Default']).toBeDefined();

    // Check formatting
    expect(formatColorSchemeConfig(schemes)).toEqual(file);
});

[evidence record sha256:69dfd29a3841699a39e5ff68826d9d5a67e40e5fae80249e3c55a5df02f2235d kind tool-call:read]
step 3: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/dynamic-theme.ts"}
tool read ok: import type {DynamicThemeFix} from '../definitions';
import {formatCSS} from '../utils/css-text/format-css';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {formatSitesFixesConfig} from './utils/format';
import {parseSitesFixesConfig, getSitesFixesFor, getDomain} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

declare const __CHROMIUM_MV2__: boolean;
declare const __CHROMIUM_MV3__: boolean;

const dynamicThemeFixesCommands: { [key: string]: keyof DynamicThemeFix } = {
    'INVERT': 'invert',
    'CSS': 'css',
    'IGNORE INLINE STYLE': 'ignoreInlineStyle',
    'IGNORE IMAGE ANALYSIS': 'ignoreImageAnalysis',
    'IGNORE CSS URL': 'ignoreCSSUrl',
};

export function parseDynamicThemeFixes(text: string): DynamicThemeFix[] {
    return parseSitesFixesConfig<DynamicThemeFix>(text, {
        commands: Object.keys(dynamicThemeFixesCommands),
        getCommandPropName: (command) => dynamicThemeFixesCommands[command],
        parseCommandValue: (command, value) => {
            if (command !== 'CSS') {
                return value.trim();
            }
            return parseArray(value);
        },
    });
}

export function formatDynamicThemeFixes(dynamicThemeFixes: DynamicThemeFix[]): string {
    const fixes = dynamicThemeFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(dynamicThemeFixesCommands),
        getPropCommandName: (prop) => Object.entries(dynamicThemeFixesCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (prop === 'css') {
                return formatCSS(value as string);
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'css') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

export function getDynamicThemeFixesFor(url: string, text: string, index: SiteFixesIndex, enabledForPDF: boolean): DynamicThemeFix[] | null {
    const fixes = getSitesFixesFor(url, text, index, parseDynamicThemeFixes);

    if (fixes.length === 0 || fixes[0].url[0] !== '*') {
        return null;
    }

    if (enabledForPDF) {
        // Copy part of fixes which will be mutated
        const invert = fixes[0].invert?.slice();
        const commonFix = {...fixes[0], invert};
        const pdfFixes: DynamicThemeFix[] = [
            commonFix,
            ...fixes.slice(1),
        ];

        const inversionFix = __CHROMIUM_MV2__ || __CHROMIUM_MV3__ ?
            '\nembed[type="application/pdf"][src="about:blank"] { filter: invert(100%) contrast(90%); }' :
            '\nembed[type="application/pdf"] { filter: invert(100%) contrast(90%); }';
        if (!commonFix.css.endsWith(inversionFix)) {
            commonFix.css += inversionFix;
        }

        if (['drive.google.com', 'mail.google.com'].includes(getDomain(url))) {
            const nestedInversionFix = 'div[role="dialog"] div[role="document"]';
            if (commonFix.invert.at(-1) !== nestedInversionFix) {
                commonFix.invert.push(nestedInversionFix);
            }
        }

        return pdfFixes;
    }

    return fixes;
}

[evidence record sha256:b6ccc71689d12c43bfbd9f528342ad7d536e9ff4a6991bbf19d9ef62e2d7077e kind tool-call:read]
step 4: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:ff3f92d9341051ed41e8260ffa80632b0942b40d55bc43fe1461468cd410e54f kind tool-call:read]
step 5: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/static-theme.ts"}
tool read ok: import type {Theme, StaticTheme} from '../definitions';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {createTextStyle} from './text-style';
import {formatSitesFixesConfig} from './utils/format';
import {applyColorMatrix, createFilterMatrix} from './utils/matrix';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

interface ThemeColors {
    [prop: string]: number[];
    neutralBg: number[];
    neutralText: number[];
    redBg: number[];
    redText: number[];
    greenBg: number[];
    greenText: number[];
    blueBg: number[];
    blueText: number[];
    fadeBg: number[];
    fadeText: number[];
}

const darkTheme: ThemeColors = {
    neutralBg: [16, 20, 23],
    neutralText: [167, 158, 139],
    redBg: [64, 12, 32],
    redText: [247, 142, 102],
    greenBg: [32, 64, 48],
    greenText: [128, 204, 148],
    blueBg: [32, 48, 64],
    blueText: [128, 182, 204],
    fadeBg: [16, 20, 23, 0.5],
    fadeText: [167, 158, 139, 0.5],
};

const lightTheme: ThemeColors = {
    neutralBg: [255, 242, 228],
    neutralText: [0, 0, 0],
    redBg: [255, 85, 170],
    redText: [140, 14, 48],
    greenBg: [192, 255, 170],
    greenText: [0, 128, 0],
    blueBg: [173, 215, 229],
    blueText: [28, 16, 171],
    fadeBg: [0, 0, 0, 0.5],
    fadeText: [0, 0, 0, 0.5],
};

function rgb([r, g, b, a]: number[]): string {
    if (typeof a === 'number') {
        return `rgba(${r}, ${g}, ${b}, ${a})`;
    }
    return `rgb(${r}, ${g}, ${b})`;
}

function mix(color1: number[], color2: number[], t: number): number[] {
    return color1.map((c, i) => Math.round(c * (1 - t) + color2[i] * t));
}

export default function createStaticStylesheet(config: Theme, url: string, isTopFrame: boolean, staticThemes: string, staticThemesIndex: SiteFixesIndex): string {
    const srcTheme = config.mode === 1 ? darkTheme : lightTheme;
    const theme = Object.entries(srcTheme).reduce((t, [prop, color]) => {
        const [r, g, b, a] = color;
        t[prop] = applyColorMatrix([r, g, b], createFilterMatrix({...config, mode: 0}));
        if (a !== undefined) {
            t[prop].push(a);
        }
        return t;
    }, {} as ThemeColors);

    const themes = getSitesFixesFor(url, staticThemes, staticThemesIndex, parseStaticThemes);

    const commonTheme = themes.find((t) => t.url[0] === '*');
    const siteTheme = themes.find((t) => t.url[0] !== '*');

    if (!commonTheme) {
        return '';
    }

    const lines: string[] = [];

    if (!siteTheme || !siteTheme.noCommon) {
        lines.push('/* Common theme */');
        lines.push(...ruleGenerators.map((gen) => gen(commonTheme, theme)!));
    }

    if (siteTheme) {
        lines.push(`/* Theme for ${siteTheme.url.join(' ')} */`);
        lines.push(...ruleGenerators.map((gen) => gen(siteTheme, theme)!));
    }

    if (config.useFont || config.textStroke > 0) {
        lines.push('/* Font */');
        lines.push(createTextStyle(config));
    }

    return lines
        .filter((ln) => ln)
        .join('\n');
}

function createRuleGen(getSelectors: (siteTheme: StaticTheme) => string[] | undefined, generateDeclarations: (theme: ThemeColors) => string[], modifySelector: ((s: string) => string) = (s) => s) {
    return (siteTheme: StaticTheme, themeColors: ThemeColors) => {
        const selectors = getSelectors(siteTheme);
        if (selectors == null || selectors.length === 0) {
            return null;
        }
        const lines: string[] = [];
        selectors.forEach((s, i) => {
            let ln = modifySelector(s);
            if (i < selectors.length - 1) {
                ln += ',';
            } else {
                ln += ' {';
            }
            lines.push(ln);
        });
        const declarations = generateDeclarations(themeColors);
        declarations.forEach((d) => lines.push(`    ${d} !important;`));
        lines.push('}');
        return lines.join('\n');
    };
}

const mx = {
    bg: {
        hover: 0.075,
        active: 0.1,
    },
    fg: {
        hover: 0.25,
        active: 0.5,
    },
    border: 0.5,
};

const ruleGenerators = [
    createRuleGen((t) => t.neutralBg, (t) => [`background-color: ${rgb(t.neutralBg)}`]),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(t.neutralBg)}`]),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(mix(t.neutralBg, [255, 255, 255], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(mix(t.neutralBg, [255, 255, 255], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.neutralText, (t) => [`color: ${rgb(t.neutralText)}`]),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(t.neutralText)}`]),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(mix(t.neutralText, [255, 255, 255], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(mix(t.neutralText, [255, 255, 255], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.neutralBorder, (t) => [`border-color: ${rgb(mix(t.neutralBg, t.neutralText, mx.border))}`]),

    createRuleGen((t) => t.redBg, (t) => [`background-color: ${rgb(t.redBg)}`]),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(t.redBg)}`]),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(mix(t.redBg, [255, 0, 64], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(mix(t.redBg, [255, 0, 64], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.redText, (t) => [`color: ${rgb(t.redText)}`]),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(t.redText)}`]),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.redBorder, (t) => [`border-color: ${rgb(mix(t.redBg, t.redText, mx.border))}`]),

    createRuleGen((t) => t.greenBg, (t) => [`background-color: ${rgb(t.greenBg)}`]),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(t.greenBg)}`]),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(mix(t.greenBg, [128, 255, 182], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(mix(t.greenBg, [128, 255, 182], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.greenText, (t) => [`color: ${rgb(t.greenText)}`]),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(t.greenText)}`]),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.greenBorder, (t) => [`border-color: ${rgb(mix(t.greenBg, t.greenText, mx.border))}`]),

    createRuleGen((t) => t.blueBg, (t) => [`background-color: ${rgb(t.blueBg)}`]),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(t.blueBg)}`]),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(mix(t.blueBg, [0, 128, 255], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(mix(t.blueBg, [0, 128, 255], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.blueText, (t) => [`color: ${rgb(t.blueText)}`]),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(t.blueText)}`]),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.blueBorder, (t) => [`border-color: ${rgb(mix(t.blueBg, t.blueText, mx.border))}`]),

    createRuleGen((t) => t.fadeBg, (t) => [`background-color: ${rgb(t.fadeBg)}`]),
    createRuleGen((t) => t.fadeText, (t) => [`color: ${rgb(t.fadeText)}`]),
    createRuleGen((t) => t.transparentBg, () => ['background-color: transparent']),
    createRuleGen((t) => t.noImage, () => ['background-image: none']),
    createRuleGen((t) => t.invert, () => ['filter: invert(100%) hue-rotate(180deg)']),
];

const staticThemeCommands: { [key: string]: keyof StaticTheme } = {
    'NO COMMON': 'noCommon',

    'NEUTRAL BG': 'neutralBg',
    'NEUTRAL BG ACTIVE': 'neutralBgActive',
    'NEUTRAL TEXT': 'neutralText',
    'NEUTRAL TEXT ACTIVE': 'neutralTextActive',
    'NEUTRAL BORDER': 'neutralBorder',

    'RED BG': 'redBg',
    'RED BG ACTIVE': 'redBgActive',
    'RED TEXT': 'redText',
    'RED TEXT ACTIVE': 'redTextActive',
    'RED BORDER': 'redBorder',

    'GREEN BG': 'greenBg',
    'GREEN BG ACTIVE': 'greenBgActive',
    'GREEN TEXT': 'greenText',
    'GREEN TEXT ACTIVE': 'greenTextActive',
    'GREEN BORDER': 'greenBorder',

    'BLUE BG': 'blueBg',
    'BLUE BG ACTIVE': 'blueBgActive',
    'BLUE TEXT': 'blueText',
    'BLUE TEXT ACTIVE': 'blueTextActive',
    'BLUE BORDER': 'blueBorder',

    'FADE BG': 'fadeBg',
    'FADE TEXT': 'fadeText',
    'TRANSPARENT BG': 'transparentBg',

    'NO IMAGE': 'noImage',
    'INVERT': 'invert',
};

export function parseStaticThemes($themes: string): StaticTheme[] {
    return parseSitesFixesConfig<StaticTheme>($themes, {
        commands: Object.keys(staticThemeCommands),
        getCommandPropName: (command) => staticThemeCommands[command],
        parseCommandValue: (command, value) => {
            if (command === 'NO COMMON') {
                return true;
            }
            return parseArray(value);
        },
    });
}

function camelCaseToUpperCase(text: string): string {
    return text.replace(/([a-z])([A-Z])/g, '$1 $2').toUpperCase();
}

export function formatStaticThemes(staticThemes: StaticTheme[]): string {
    const themes = staticThemes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(themes, {
        props: Object.values(staticThemeCommands),
        getPropCommandName: camelCaseToUpperCase,
        formatPropValue: (prop, value) => {
            if (prop === 'noCommon') {
                return '';
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'noCommon') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

[evidence record sha256:e54784d9c5e1e3031a076f37517b830cba77bbfb5ae5946224257afb01c51bb6 kind tool-call:read]
step 6: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/css-filter.ts"}
tool read ok: import type {Theme, InversionFix} from '../definitions';
import {compareChromeVersions, chromiumVersion, isFirefox, firefoxVersion} from '../utils/platform';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns, isURLInList} from '../utils/url';

import {createTextStyle} from './text-style';
import {formatSitesFixesConfig} from './utils/format';
import {applyColorMatrix, createFilterMatrix} from './utils/matrix';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

declare const __CHROMIUM_MV2__: boolean;
declare const __CHROMIUM_MV3__: boolean;

export enum FilterMode {
    light = 0,
    dark = 1
}

/**
 * This checks if the current chromium version has the patch in it.
 * As of Chromium v81.0.4035.0 this has been the situation
 *
 * Bug report: https://bugs.chromium.org/p/chromium/issues/detail?id=501582
 * Patch: https://chromium-review.googlesource.com/c/chromium/src/+/1979258
 */
export function hasPatchForChromiumIssue501582(): boolean {
    return __CHROMIUM_MV3__ || Boolean(
        __CHROMIUM_MV2__ &&
        compareChromeVersions(chromiumVersion, '81.0.4035.0') >= 0
    );
}

/**
 * Since Firefox v102.0, they have changed to the new root behavior.
 * This was already the case for Chromium v81.0.4035.0 and Firefox now
 * switched over as well.
 */
export function hasFirefoxNewRootBehavior(): boolean {
    return Boolean(
        isFirefox &&
        compareChromeVersions(firefoxVersion, '102.0') >= 0
    );
}

export default function createCSSFilterStyleSheet(config: Theme, url: string, isTopFrame: boolean, fixes: string, index: SiteFixesIndex): string {
    const filterValue = getCSSFilterValue(config)!;
    const reverseFilterValue = 'invert(100%) hue-rotate(180deg)';
    return cssFilterStyleSheetTemplate('html', filterValue, reverseFilterValue, config, url, isTopFrame, fixes, index);
}

export function cssFilterStyleSheetTemplate(filterRoot: string, filterValue: string, reverseFilterValue: string, config: Theme, url: string, isTopFrame: boolean, fixes: string, index: SiteFixesIndex): string {
    const fix = getInversionFixesFor(url, fixes, index);

    const lines: string[] = [];

    lines.push('@media screen {');

    // Add leading rule
    if (filterValue && isTopFrame) {
        lines.push('');
        lines.push('/* Leading rule */');
        lines.push(createLeadingRule(filterRoot, filterValue));
    }

    if (config.mode === FilterMode.dark) {
        // Add reverse rule
        lines.push('');
        lines.push('/* Reverse rule */');
        lines.push(createReverseRule(reverseFilterValue, fix));
    }

    if (config.useFont || config.textStroke > 0) {
        // Add text rule
        lines.push('');
        lines.push('/* Font */');
        lines.push(createTextStyle(config));
    }

    // Full screen fix
    lines.push('');
    lines.push('/* Full screen */');
    [':-webkit-full-screen', ':-moz-full-screen', ':fullscreen'].forEach((fullScreen) => {
        lines.push(`${fullScreen}, ${fullScreen} * {`);
        lines.push('  -webkit-filter: none !important;');
        lines.push('  filter: none !important;');
        lines.push('}');
    });

    if (isTopFrame) {
        const light: [number, number, number] = [255, 255, 255];
        // If browser affected by Chromium Issue 501582, set dark background on html
        // Or if browser is Firefox v102+
        const bgColor = (!hasPatchForChromiumIssue501582() && !hasFirefoxNewRootBehavior()) && config.mode === FilterMode.dark ?
            applyColorMatrix(light, createFilterMatrix(config)).map(Math.round) :
            light;
        lines.push('');
        lines.push('/* Page background */');
        lines.push('html {');
        lines.push(`  background: rgb(${bgColor.join(',')}) !important;`);
        lines.push('}');
    }

    if (fix.css && fix.css.length > 0 && config.mode === FilterMode.dark) {
        lines.push('');
        lines.push('/* Custom rules */');
        lines.push(fix.css);
    }

    lines.push('');
    lines.push('}');

    return lines.join('\n');
}

export function getCSSFilterValue(config: Theme): string | null {
    const filters: string[] = [];

    if (config.mode === FilterMode.dark) {
        filters.push('invert(100%) hue-rotate(180deg)');
    }
    if (config.brightness !== 100) {
        filters.push(`brightness(${config.brightness}%)`);
    }
    if (config.contrast !== 100) {
        filters.push(`contrast(${config.contrast}%)`);
    }
    if (config.grayscale !== 0) {
        filters.push(`grayscale(${config.grayscale}%)`);
    }
    if (config.sepia !== 0) {
        filters.push(`sepia(${config.sepia}%)`);
    }

    if (filters.length === 0) {
        return null;
    }

    return filters.join(' ');
}

function createLeadingRule(filterRoot: string, filterValue: string): string {
    return [
        `${filterRoot} {`,
        `  -webkit-filter: ${filterValue} !important;`,
        `  filter: ${filterValue} !important;`,
        '}',
    ].join('\n');
}

function joinSelectors(selectors: string[]): string {
    return selectors.map((s) => s.replace(/\,$/, '')).join(',\n');
}

function createReverseRule(reverseFilterValue: string, fix: InversionFix): string {
    const lines: string[] = [];

    if (fix.invert.length > 0) {
        lines.push(`${joinSelectors(fix.invert)} {`);
        lines.push(`  -webkit-filter: ${reverseFilterValue} !important;`);
        lines.push(`  filter: ${reverseFilterValue} !important;`);
        lines.push('}');
    }

    if (fix.noinvert.length > 0) {
        lines.push(`${joinSelectors(fix.noinvert)} {`);
        lines.push('  -webkit-filter: none !important;');
        lines.push('  filter: none !important;');
        lines.push('}');
    }

    if (fix.removebg.length > 0) {
        lines.push(`${joinSelectors(fix.removebg)} {`);
        lines.push('  background: white !important;');
        lines.push('}');
    }

    return lines.join('\n');
}

/**
* Returns fixes for a given URL.
* If no matches found, common fixes will be returned.
* @param url Site URL.
* @param inversionFixes List of inversion fixes.
*/
export function getInversionFixesFor(url: string, fixes: string, index: SiteFixesIndex): InversionFix {
    const inversionFixes = getSitesFixesFor(url, fixes, index, parseInversionFixes);

    const common = {
        url: inversionFixes[0].url,
        invert: inversionFixes[0].invert || [],
        noinvert: inversionFixes[0].noinvert || [],
        removebg: inversionFixes[0].removebg || [],
        css: inversionFixes[0].css || '',
    };

    if (url) {
        // Search for match with given URL
        const matches = inversionFixes
            .slice(1)
            .filter((s) => isURLInList(url, s.url))
            .sort((a, b) => b.url[0].length - a.url[0].length);
        if (matches.length > 0) {
            const found = matches[0];
            return {
                url: found.url,
                invert: common.invert.concat(found.invert || []),
                noinvert: common.noinvert.concat(found.noinvert || []),
                removebg: common.removebg.concat(found.removebg || []),
                css: [common.css, found.css].filter((s) => s).join('\n'),
            };
        }
    }
    return common;
}

const inversionFixesCommands: { [key: string]: keyof InversionFix } = {
    'INVERT': 'invert',
    'NO INVERT': 'noinvert',
    'REMOVE BG': 'removebg',
    'CSS': 'css',
};

export function parseInversionFixes(text: string): InversionFix[] {
    return parseSitesFixesConfig<InversionFix>(text, {
        commands: Object.keys(inversionFixesCommands),
        getCommandPropName: (command) => inversionFixesCommands[command],
        parseCommandValue: (command, value) => {
            if (command === 'CSS') {
                return value.trim();
            }
            return parseArray(value);
        },
    });
}

export function formatInversionFixes(inversionFixes: InversionFix[]): string {
    const fixes = inversionFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(inversionFixesCommands),
        getPropCommandName: (prop) => Object.entries(inversionFixesCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (prop === 'css') {
                return (value as string).trim().replace(/\n+/g, '\n');
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'css') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

[evidence record sha256:8870a25882b7f7fd99e1cb123221a2df6c9b86afbacc4d289bd06b221e97fc41 kind tool-call:read]
step 7: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/detector-hints.ts"}
tool read ok: import type {DetectorHint} from '../definitions';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {formatSitesFixesConfig} from './utils/format';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex, SitesFixesParserOptions} from './utils/parse';

const detectorHintsCommands: { [key: string]: keyof DetectorHint } = {
    'TARGET': 'target',
    'MATCH': 'match',
    'NO DARK THEME': 'noDarkTheme',
    'SYSTEM THEME': 'systemTheme',
    'IFRAME': 'iframe',
};

const detectorParserOptions: SitesFixesParserOptions<DetectorHint> = {
    commands: Object.keys(detectorHintsCommands),
    getCommandPropName: (command) => detectorHintsCommands[command],
    parseCommandValue: (command, value) => {
        if (command === 'TARGET') {
            return value.trim();
        }
        if (command === 'NO DARK THEME' || command === 'SYSTEM THEME') {
            return true;
        }
        return parseArray(value);
    },
};

export function parseDetectorHints(text: string): DetectorHint[] {
    return parseSitesFixesConfig<DetectorHint>(text, detectorParserOptions);
}

export function formatDetectorHints(detectorHints: DetectorHint[]): string {
    const fixes = detectorHints.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(detectorHintsCommands),
        getPropCommandName: (prop) => Object.entries(detectorHintsCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (Array.isArray(value)) {
                return formatArray(value).trim();
            }
            if (prop === 'noDarkTheme' || prop === 'systemTheme') {
                return '';
            }
            return String(value).trim();
        },
        shouldIgnoreProp: (_prop, value) => {
            return !value;
        },
    });
}

export function getDetectorHintsFor(url: string, text: string, index: SiteFixesIndex): DetectorHint[] | null {
    const fixes = getSitesFixesFor(url, text, index, parseDetectorHints);

    if (fixes.length === 0) {
        return null;
    }

    return fixes;
}

[evidence record sha256:6ef818b3498154258f60cc35276e965e21e9897c71eef547c2c767af7d848424 kind tool-call:read]
step 8: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/format.ts"}
tool read ok: import {push} from '../../utils/array';

interface SiteFix {
    url: string[];
    [prop: string]: any;
}

interface SitesFixesFormatOptions {
    props: string[];
    getPropCommandName: (prop: string) => string;
    formatPropValue: (prop: string, value: string | string[]) => string;
    shouldIgnoreProp: (prop: string, value: string | string[]) => boolean;
}

export function formatSitesFixesConfig(fixes: SiteFix[], options: SitesFixesFormatOptions): string {
    const lines: string[] = [];

    fixes.forEach((fix, i) => {
        push(lines, fix.url);
        options.props.forEach((prop) => {
            const command = options.getPropCommandName(prop);
            const value = fix[prop];
            if (options.shouldIgnoreProp(prop, value)) {
                return;
            }
            lines.push('');
            lines.push(command);
            const formattedValue = options.formatPropValue(prop, value);
            if (formattedValue) {
                lines.push(formattedValue);
            }
        });
        if (i < fixes.length - 1) {
            lines.push('');
            lines.push('='.repeat(32));
            lines.push('');
        }
    });

    lines.push('');
    return lines.join('\n');
}

[evidence record sha256:26d2683577cf4091ff395478d5de67db0c74dca867cde2d0fd1fe85f5d6fe8c6 kind tool-call:read]
step 9: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.tests.ts"}
tool read failed: failed: ENOENT: no such file or directory, open '/work/src/generators/utils/parse.tests.ts', caused by ENOENT
[evidence record sha256:f2d111c68a4455bb12e1bdefaabe161eab21c800167a405680db809ec604d921 kind tool-call:read]
step 10: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:ca7f9daa9e80b69cfb9f2c71adc7d312804ab11646450864944c51f93e5a0407 kind tool-call:read]
step 11: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:34dce5b47dd19c03f1129e472075efb715111bd06d8e7350e3ffd4f9279ede76 kind tool-call:read]
step 12: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:00295109c96b38528d9f2d83f76f6b61f21c4a0b31019b12d03c7c3ed3ba20b9 kind tool-call:read]
step 13: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:a15fc30f1561b28c2b3b91ff48fa25afb2db6a24ca05f2edfb669aeb36b16e6f kind tool-call:read]
step 14: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:8264ec722df1c16ec66d3a5646757abe891a712ee9369595b9020822a2b15a0f kind tool-call:read]
step 15: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:594dee1c20f8f733df0ce510b228862c3f851d60f52a9bb976946af1384a5cb3 kind tool-call:read]
step 16: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:771cd06b613fa3e52db3ccc9e27b131fc662f263e427efdedfd5f0bcc6260fdc kind tool-call:read]
step 17: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:91b9c02852a3bdb1380fc57e61b84662e1387e2e615e77f3943e76c3ffb5135b kind tool-call:read]
step 18: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:3a8aa16b19e802e41a194ce9bbccb9449ef9c20dd6b1f271d0221b3dd3280a98 kind tool-call:read]
step 19: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:7d66feb59d22514946188bf275c893781e0e5d9d1ba9b915e75f5fb3b36817a7 kind tool-call:read]
step 20: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:2df818243f732fa1197349d2e63b2ba0530e1698562bde005392ad29232955b7 kind tool-call:read]
step 21: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:f90d86176d16c762e91f5ebea222fa12163381ebb66acef5745ac315d29b00c7 kind tool-call:read]
step 22: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:6e6a22d3dedb35c92ddf865a280fe42ee949744d5ec1ccc39d1e7489fe3bc283 kind tool-call:read]
step 23: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:0f0c529bea1630bf104fc77102fc0ed88a73fd2e2e767e4065ee4347407f20d8 kind tool-call:read]
step 24: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:f3273ba41eca0869c3de7ca6cda2ed44d343948bea38ae9a16912c5762c04d9f kind tool-call:read]
step 25: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:479bdf0f71486bed58187dceaf4f7b23a3f9bf60b29ffee7b369d11649a05c4b kind tool-call:read]
step 26: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:22de26e4a5fccfbc707c1c1253beb19b71645a331d1dac882ad4e55982d546c4 kind tool-call:read]
step 27: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:c9b54376b675570cb52fbe0cba8a1c0903e3466e220deb635cab2abfc98897ca kind tool-call:read]
step 28: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:5376121046c86beb948e7b25dbc99e97ed802a7ef44f05eb824669c3aece0e3c kind tool-call:read]
step 29: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:49a7ace70211c27454ddc86fbdb9271ad8e44af82d490a2f608709c8d8585483 kind tool-call:read]
step 30: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:7e9686e74945a78107925c9c03c11dd02231501db847ec77906639643ec2ee15 kind tool-call:read]
step 31: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:e57e331f1e4e5c9e56dd9669a7177d3db4218f412780d51f3f6a114dcd20d10d kind tool-call:read]
step 32: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:01c11fd6aa70a4796f18155d36a316d4cf79e27825058ff225c434d461a46075 kind tool-call:read]
step 33: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:9c6a925382aff24b248b42c1a63a2c84e29df4360d56c1caefcbfe6895cc0d79 kind tool-call:read]
step 34: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:6e2e32baa8e4560e1da0b1b13cffdc1667ba70df3d0fc4a57620a3a7c20a25e2 kind tool-call:read]
step 35: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:763749c0fd5ddfec174d3064a1efd51567484196b527e04c7fb3922d7f1acd38 kind tool-call:read]
stopped: max-tokens after 35 steps, 1008501 tokens
gate typecheck not-applicable: package.json declares no typecheck script [evidence record sha256:2937696362f6458fe282589c221bc4c8fee1e95f3a2fcdf7bdda33a9c257023a]
gate lint passed: the command exited 0 [evidence record sha256:26c54c9197125e4ebe1c7663164f927d71598abb55d531348135e4ad21615ca4]
gate format not-applicable: package.json declares no check-only format script, and running a writing formatter as a gate would edit the tree it is judging [evidence record sha256:9b21ee9385ec49965029e8ae457e7fa57f61ec2cc75ef84219ddfa2809af47cb]
gate tests failed: the command exited 1 [evidence record sha256:87c75ab3183489340c85b413e3b4dceb88adca071143db5bfbd67345f666354d]
gate file-set passed: nothing changed and no file set was declared, so there is nothing to check [evidence record sha256:da941441683d2a932e3656d29e47bf1d3dd8e8827a4fa28340e9192147fc0140]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:04cbfbf05767e77d5767fb5772e9a7e62cec4ae74d421e013fe257c5eccb892c]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:1205c218e3298f2b2a033cfedf23b7ee3ea0c6b62794e4df970e745e5f7a0cb4]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:6c58dc822ecce8f4da2f5c78e5027adb86eef9574d10c896a93cc27d285aa5df]
gate diff-budget passed (advisory): within budget: 0 file(s) and 0 added line(s) [evidence record sha256:e3dbe12b4205d31760f1fad23748c5ea393fa58eb135b7d0aec249a7ae178e24]
auto-resolve attempt 1 of 2
step 1: calling local:qwen3.8:27b
tool read <- {"path":"tests/unit/config/config.tests.ts"}
tool read ok: import {readFile} from 'node:fs';

import type {StaticTheme} from '../../../src/definitions';
import {parseInversionFixes, formatInversionFixes} from '../../../src/generators/css-filter';
import {parseDetectorHints, formatDetectorHints} from '../../../src/generators/detector-hints';
import {parseDynamicThemeFixes, formatDynamicThemeFixes} from '../../../src/generators/dynamic-theme';
import {parseStaticThemes, formatStaticThemes} from '../../../src/generators/static-theme';
import {parseColorSchemeConfig} from '../../../src/utils/colorscheme-parser';
import type {ParsedColorSchemeConfig} from '../../../src/utils/colorscheme-parser';
import {parseArray, formatArray, getTextDiffIndex, getTextPositionMessage} from '../../../src/utils/text';
import {compareURLPatterns} from '../../../src/utils/url';
import {rootPath} from '../../support/test-utils';

function readConfig(fileName: string) {
    return new Promise<string>((resolve, reject) => {
        readFile(rootPath('src/config', fileName), {encoding: 'utf-8'}, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            resolve(data);
        });
    });
}

function isURLPatternValid(url: string) {
    return url.length > 0 && url.indexOf('://') < 0;
}

function throwIfDifferent(input: string, expected: string, message: string) {
    return () => {
        const diffIndex = getTextDiffIndex(input, expected);
        if (diffIndex >= 0) {
            throw new Error(`${message}\n${getTextPositionMessage(input, diffIndex)}`);
        }
    };
}

function formatColorSchemeConfig(scheme: ParsedColorSchemeConfig): string {
    const names = Object.keys(scheme.dark);
    const lines = [];
    for (const name of names) {
        lines.push(name);
        lines.push('');
        for (const color of ['dark', 'light']) {
            const style = scheme[color as keyof ParsedColorSchemeConfig][name];
            if (style) {
                const {backgroundColor, textColor} = style;
                lines.push(color.toUpperCase());
                if (backgroundColor) {
                    lines.push(`background: ${backgroundColor.toLowerCase()}`);
                }
                if (textColor) {
                    lines.push(`text: ${textColor.toLowerCase()}`);
                }
                lines.push('');
            }
        }
        lines.push('='.repeat(32));
        lines.push('');
    }
    lines.pop();
    lines.pop();
    return lines.join('\n');
}

test('Dark Sites list', async () => {
    const file = await readConfig('dark-sites.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const sites = parseArray(file);

    // is not empty
    expect(sites.length).toBeGreaterThan(0);

    // url patterns should have no protocol
    expect(sites.every(isURLPatternValid)).toBe(true);

    // sites are sorted alphabetically
    expect(sites.slice().sort(compareURLPatterns)).toEqual(sites);

    // sites are properly formatted
    expect(throwIfDifferent(file, formatArray(sites), 'Dark Sites list format error')).not.toThrow();
});

test('Detector Hints config', async () => {
    const file = await readConfig('detector-hints.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const hints = parseDetectorHints(file);

    // each hint has valid URL
    expect(hints.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // hints are sorted alphabetically
    expect(hints.map(({url}) => url[0])).toEqual(hints.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    expect(hints.every(({target, match}) => ![target].concat(match).some((s) => commaSelector.test(s)))).toBe(true);

    // only a single selector is allowed for target
    expect(hints.every(({target, noDarkTheme, systemTheme}) => noDarkTheme || systemTheme || typeof target === 'string' && !target.includes('\n'))).toBe(true);

    // hints are properly formatted
    expect(throwIfDifferent(file, formatDetectorHints(hints), 'Detector Hints format error')).not.toThrow();

    // should parse empty config
    expect(parseDetectorHints('')).toEqual([]);

    // should skip unsupported commands
    expect(parseDetectorHints([
        'inbox.google.com',
        'mail.google.com',
        'TARGET', 'a',
        'MATCH', '.b', '#c',
        'UNSUPPORTED', 'c',
        '========',
        'proton.me',
        'SYSTEM THEME',
        '========',
        'twitter.com',
        'UNSUPPORTED', 'a', 'b',
        'TARGET', 'c',
        'MATCH', '[d="e"]',
        '========',
        'wikipedia.org',
        'NO DARK THEME',
    ].join('\n'))).toEqual([
        {url: ['inbox.google.com', 'mail.google.com'], target: 'a', match: ['.b', '#c']},
        {url: ['proton.me'], systemTheme: true},
        {url: ['twitter.com'], target: 'c', match: ['[d="e"]']},
        {url: ['wikipedia.org'], noDarkTheme: true},
    ] as any);
});

test('Dynamic Theme Fixes config', async () => {
    const file = await readConfig('dynamic-theme-fixes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const fixes = parseDynamicThemeFixes(file);

    // there is a common fix
    expect(fixes[0].url[0]).toEqual('*');

    // each fix has valid URL
    expect(fixes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // fixes are sorted alphabetically
    expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    expect(fixes.every(({invert, ignoreInlineStyle, ignoreImageAnalysis}) => (invert || []).concat(ignoreInlineStyle || []).concat(ignoreImageAnalysis || []).every((s) => !commaSelector.test(s)))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatDynamicThemeFixes(fixes), 'Dynamic fixes format error')).not.toThrow();

    // should parse empty config
    expect(parseDynamicThemeFixes('')).toEqual([]);

    // should skip unsupported commands
    expect(parseDynamicThemeFixes([
        'inbox.google.com',
        'mail.google.com',
        'INVERT', 'a', 'b',
        'CSS', '.x { color: white !important; }',
        'UNSUPPORTED', 'c', 'd',
        '========',
        'twitter.com',
        'UNSUPPORTED', 'a', 'b',
        'INVERT', 'c', 'd',
        '========',
        'wikipedia.org',
        'IGNORE INLINE STYLE', 'a', 'b',
        '========',
        'duckduckgo.com',
        'IGNORE IMAGE ANALYSIS', 'img[alt="Logo"]', 'canvas',
    ].join('\n'))).toEqual([
        {url: ['inbox.google.com', 'mail.google.com'], invert: ['a', 'b'], css: '.x { color: white !important; }'},
        {url: ['twitter.com'], invert: ['c', 'd']},
        {url: ['wikipedia.org'], ignoreInlineStyle: ['a', 'b']},
        {url: ['duckduckgo.com'], ignoreImageAnalysis: ['img[alt="Logo"]', 'canvas']},
    ] as any);
});

test('Inversion Fixes config', async () => {
    const file = await readConfig('inversion-fixes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const fixes = parseInversionFixes(file);

    // there is a common fix
    expect(fixes[0].url[0]).toEqual('*');

    // each fix has valid URL
    expect(fixes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // fixes are sorted alphabetically
    expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    expect(fixes.every(({invert, noinvert, removebg}) => (invert || []).concat(noinvert || []).concat(removebg || []).every((s) => s.indexOf(',') < 0))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatInversionFixes(fixes), 'Inversion fixes format error')).not.toThrow();
});

test('Static Themes config', async () => {
    const file = await readConfig('static-themes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const themes = parseStaticThemes(file);

    // there is a common theme
    expect(themes[0].url[0]).toEqual('*');

    // each theme has valid URL
    expect(themes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // themes are sorted alphabetically
    expect(themes.map(({url}) => url[0])).toEqual(themes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    expect(themes.every((t) => (Object.keys(t) as Array<keyof StaticTheme>)
        .filter((prop) => ['url', 'noCommon'].indexOf(prop) < 0)
        .every((prop) => (t[prop] as string[])
            .every((s) => s.indexOf(',') < 0)))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatStaticThemes(themes), 'Static theme format error')).not.toThrow();
});

test('Colorscheme config', async () => {
    const file = await readConfig('color-schemes.drconf');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const {result: schemes, error} = parseColorSchemeConfig(file);

    // Their is no error
    expect(error).toBeNull();

    // There is a default Dark color scheme
    expect(schemes.dark['Default']).toBeDefined();

    // There is a default Light color scheme
    expect(schemes.light['Default']).toBeDefined();

    // Check formatting
    expect(formatColorSchemeConfig(schemes)).toEqual(file);
});

[evidence record sha256:bc56ca781f79131dde5fdc04fa044d6d017bbcf5677d4fdae37498c5ed083068 kind tool-call:read]
tool list <- {}
tool list ok: .git/
.gitattributes
.github/
.gitignore
.npmignore
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
README.md
SECURITY.md
docs/
eslint-plugin-local.js
eslint.config.js
index.d.ts
integrity/
node_modules/
package-lock.json
package.json
src/
tasks/
tests/
[evidence record sha256:bb3f21acb2f3dc72f10493ba57a6fafbcbac3414d0ba10e55753529f35e08d7a kind tool-call:list]
step 2: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/dynamic-theme.ts"}
tool read ok: import type {DynamicThemeFix} from '../definitions';
import {formatCSS} from '../utils/css-text/format-css';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {formatSitesFixesConfig} from './utils/format';
import {parseSitesFixesConfig, getSitesFixesFor, getDomain} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

declare const __CHROMIUM_MV2__: boolean;
declare const __CHROMIUM_MV3__: boolean;

const dynamicThemeFixesCommands: { [key: string]: keyof DynamicThemeFix } = {
    'INVERT': 'invert',
    'CSS': 'css',
    'IGNORE INLINE STYLE': 'ignoreInlineStyle',
    'IGNORE IMAGE ANALYSIS': 'ignoreImageAnalysis',
    'IGNORE CSS URL': 'ignoreCSSUrl',
};

export function parseDynamicThemeFixes(text: string): DynamicThemeFix[] {
    return parseSitesFixesConfig<DynamicThemeFix>(text, {
        commands: Object.keys(dynamicThemeFixesCommands),
        getCommandPropName: (command) => dynamicThemeFixesCommands[command],
        parseCommandValue: (command, value) => {
            if (command !== 'CSS') {
                return value.trim();
            }
            return parseArray(value);
        },
    });
}

export function formatDynamicThemeFixes(dynamicThemeFixes: DynamicThemeFix[]): string {
    const fixes = dynamicThemeFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(dynamicThemeFixesCommands),
        getPropCommandName: (prop) => Object.entries(dynamicThemeFixesCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (prop === 'css') {
                return formatCSS(value as string);
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'css') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

export function getDynamicThemeFixesFor(url: string, text: string, index: SiteFixesIndex, enabledForPDF: boolean): DynamicThemeFix[] | null {
    const fixes = getSitesFixesFor(url, text, index, parseDynamicThemeFixes);

    if (fixes.length === 0 || fixes[0].url[0] !== '*') {
        return null;
    }

    if (enabledForPDF) {
        // Copy part of fixes which will be mutated
        const invert = fixes[0].invert?.slice();
        const commonFix = {...fixes[0], invert};
        const pdfFixes: DynamicThemeFix[] = [
            commonFix,
            ...fixes.slice(1),
        ];

        const inversionFix = __CHROMIUM_MV2__ || __CHROMIUM_MV3__ ?
            '\nembed[type="application/pdf"][src="about:blank"] { filter: invert(100%) contrast(90%); }' :
            '\nembed[type="application/pdf"] { filter: invert(100%) contrast(90%); }';
        if (!commonFix.css.endsWith(inversionFix)) {
            commonFix.css += inversionFix;
        }

        if (['drive.google.com', 'mail.google.com'].includes(getDomain(url))) {
            const nestedInversionFix = 'div[role="dialog"] div[role="document"]';
            if (commonFix.invert.at(-1) !== nestedInversionFix) {
                commonFix.invert.push(nestedInversionFix);
            }
        }

        return pdfFixes;
    }

    return fixes;
}

[evidence record sha256:1615c3020d3f322517782838789015f1a2992944392c54a50ed79eec09a73da0 kind tool-call:read]
step 3: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:d2bac06dbbc05a91d1967baa61ed7e6a82d8c1824e04c44a55dc739d2107fbe3 kind tool-call:read]
step 4: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/static-theme.ts"}
tool read ok: import type {Theme, StaticTheme} from '../definitions';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {createTextStyle} from './text-style';
import {formatSitesFixesConfig} from './utils/format';
import {applyColorMatrix, createFilterMatrix} from './utils/matrix';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

interface ThemeColors {
    [prop: string]: number[];
    neutralBg: number[];
    neutralText: number[];
    redBg: number[];
    redText: number[];
    greenBg: number[];
    greenText: number[];
    blueBg: number[];
    blueText: number[];
    fadeBg: number[];
    fadeText: number[];
}

const darkTheme: ThemeColors = {
    neutralBg: [16, 20, 23],
    neutralText: [167, 158, 139],
    redBg: [64, 12, 32],
    redText: [247, 142, 102],
    greenBg: [32, 64, 48],
    greenText: [128, 204, 148],
    blueBg: [32, 48, 64],
    blueText: [128, 182, 204],
    fadeBg: [16, 20, 23, 0.5],
    fadeText: [167, 158, 139, 0.5],
};

const lightTheme: ThemeColors = {
    neutralBg: [255, 242, 228],
    neutralText: [0, 0, 0],
    redBg: [255, 85, 170],
    redText: [140, 14, 48],
    greenBg: [192, 255, 170],
    greenText: [0, 128, 0],
    blueBg: [173, 215, 229],
    blueText: [28, 16, 171],
    fadeBg: [0, 0, 0, 0.5],
    fadeText: [0, 0, 0, 0.5],
};

function rgb([r, g, b, a]: number[]): string {
    if (typeof a === 'number') {
        return `rgba(${r}, ${g}, ${b}, ${a})`;
    }
    return `rgb(${r}, ${g}, ${b})`;
}

function mix(color1: number[], color2: number[], t: number): number[] {
    return color1.map((c, i) => Math.round(c * (1 - t) + color2[i] * t));
}

export default function createStaticStylesheet(config: Theme, url: string, isTopFrame: boolean, staticThemes: string, staticThemesIndex: SiteFixesIndex): string {
    const srcTheme = config.mode === 1 ? darkTheme : lightTheme;
    const theme = Object.entries(srcTheme).reduce((t, [prop, color]) => {
        const [r, g, b, a] = color;
        t[prop] = applyColorMatrix([r, g, b], createFilterMatrix({...config, mode: 0}));
        if (a !== undefined) {
            t[prop].push(a);
        }
        return t;
    }, {} as ThemeColors);

    const themes = getSitesFixesFor(url, staticThemes, staticThemesIndex, parseStaticThemes);

    const commonTheme = themes.find((t) => t.url[0] === '*');
    const siteTheme = themes.find((t) => t.url[0] !== '*');

    if (!commonTheme) {
        return '';
    }

    const lines: string[] = [];

    if (!siteTheme || !siteTheme.noCommon) {
        lines.push('/* Common theme */');
        lines.push(...ruleGenerators.map((gen) => gen(commonTheme, theme)!));
    }

    if (siteTheme) {
        lines.push(`/* Theme for ${siteTheme.url.join(' ')} */`);
        lines.push(...ruleGenerators.map((gen) => gen(siteTheme, theme)!));
    }

    if (config.useFont || config.textStroke > 0) {
        lines.push('/* Font */');
        lines.push(createTextStyle(config));
    }

    return lines
        .filter((ln) => ln)
        .join('\n');
}

function createRuleGen(getSelectors: (siteTheme: StaticTheme) => string[] | undefined, generateDeclarations: (theme: ThemeColors) => string[], modifySelector: ((s: string) => string) = (s) => s) {
    return (siteTheme: StaticTheme, themeColors: ThemeColors) => {
        const selectors = getSelectors(siteTheme);
        if (selectors == null || selectors.length === 0) {
            return null;
        }
        const lines: string[] = [];
        selectors.forEach((s, i) => {
            let ln = modifySelector(s);
            if (i < selectors.length - 1) {
                ln += ',';
            } else {
                ln += ' {';
            }
            lines.push(ln);
        });
        const declarations = generateDeclarations(themeColors);
        declarations.forEach((d) => lines.push(`    ${d} !important;`));
        lines.push('}');
        return lines.join('\n');
    };
}

const mx = {
    bg: {
        hover: 0.075,
        active: 0.1,
    },
    fg: {
        hover: 0.25,
        active: 0.5,
    },
    border: 0.5,
};

const ruleGenerators = [
    createRuleGen((t) => t.neutralBg, (t) => [`background-color: ${rgb(t.neutralBg)}`]),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(t.neutralBg)}`]),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(mix(t.neutralBg, [255, 255, 255], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(mix(t.neutralBg, [255, 255, 255], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.neutralText, (t) => [`color: ${rgb(t.neutralText)}`]),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(t.neutralText)}`]),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(mix(t.neutralText, [255, 255, 255], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(mix(t.neutralText, [255, 255, 255], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.neutralBorder, (t) => [`border-color: ${rgb(mix(t.neutralBg, t.neutralText, mx.border))}`]),

    createRuleGen((t) => t.redBg, (t) => [`background-color: ${rgb(t.redBg)}`]),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(t.redBg)}`]),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(mix(t.redBg, [255, 0, 64], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(mix(t.redBg, [255, 0, 64], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.redText, (t) => [`color: ${rgb(t.redText)}`]),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(t.redText)}`]),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.redBorder, (t) => [`border-color: ${rgb(mix(t.redBg, t.redText, mx.border))}`]),

    createRuleGen((t) => t.greenBg, (t) => [`background-color: ${rgb(t.greenBg)}`]),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(t.greenBg)}`]),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(mix(t.greenBg, [128, 255, 182], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(mix(t.greenBg, [128, 255, 182], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.greenText, (t) => [`color: ${rgb(t.greenText)}`]),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(t.greenText)}`]),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.greenBorder, (t) => [`border-color: ${rgb(mix(t.greenBg, t.greenText, mx.border))}`]),

    createRuleGen((t) => t.blueBg, (t) => [`background-color: ${rgb(t.blueBg)}`]),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(t.blueBg)}`]),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(mix(t.blueBg, [0, 128, 255], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(mix(t.blueBg, [0, 128, 255], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.blueText, (t) => [`color: ${rgb(t.blueText)}`]),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(t.blueText)}`]),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.blueBorder, (t) => [`border-color: ${rgb(mix(t.blueBg, t.blueText, mx.border))}`]),

    createRuleGen((t) => t.fadeBg, (t) => [`background-color: ${rgb(t.fadeBg)}`]),
    createRuleGen((t) => t.fadeText, (t) => [`color: ${rgb(t.fadeText)}`]),
    createRuleGen((t) => t.transparentBg, () => ['background-color: transparent']),
    createRuleGen((t) => t.noImage, () => ['background-image: none']),
    createRuleGen((t) => t.invert, () => ['filter: invert(100%) hue-rotate(180deg)']),
];

const staticThemeCommands: { [key: string]: keyof StaticTheme } = {
    'NO COMMON': 'noCommon',

    'NEUTRAL BG': 'neutralBg',
    'NEUTRAL BG ACTIVE': 'neutralBgActive',
    'NEUTRAL TEXT': 'neutralText',
    'NEUTRAL TEXT ACTIVE': 'neutralTextActive',
    'NEUTRAL BORDER': 'neutralBorder',

    'RED BG': 'redBg',
    'RED BG ACTIVE': 'redBgActive',
    'RED TEXT': 'redText',
    'RED TEXT ACTIVE': 'redTextActive',
    'RED BORDER': 'redBorder',

    'GREEN BG': 'greenBg',
    'GREEN BG ACTIVE': 'greenBgActive',
    'GREEN TEXT': 'greenText',
    'GREEN TEXT ACTIVE': 'greenTextActive',
    'GREEN BORDER': 'greenBorder',

    'BLUE BG': 'blueBg',
    'BLUE BG ACTIVE': 'blueBgActive',
    'BLUE TEXT': 'blueText',
    'BLUE TEXT ACTIVE': 'blueTextActive',
    'BLUE BORDER': 'blueBorder',

    'FADE BG': 'fadeBg',
    'FADE TEXT': 'fadeText',
    'TRANSPARENT BG': 'transparentBg',

    'NO IMAGE': 'noImage',
    'INVERT': 'invert',
};

export function parseStaticThemes($themes: string): StaticTheme[] {
    return parseSitesFixesConfig<StaticTheme>($themes, {
        commands: Object.keys(staticThemeCommands),
        getCommandPropName: (command) => staticThemeCommands[command],
        parseCommandValue: (command, value) => {
            if (command === 'NO COMMON') {
                return true;
            }
            return parseArray(value);
        },
    });
}

function camelCaseToUpperCase(text: string): string {
    return text.replace(/([a-z])([A-Z])/g, '$1 $2').toUpperCase();
}

export function formatStaticThemes(staticThemes: StaticTheme[]): string {
    const themes = staticThemes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(themes, {
        props: Object.values(staticThemeCommands),
        getPropCommandName: camelCaseToUpperCase,
        formatPropValue: (prop, value) => {
            if (prop === 'noCommon') {
                return '';
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'noCommon') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

[evidence record sha256:f83eecadfa23c94079563914bf190a2133e77c14488675723a1118cd1e0cb006 kind tool-call:read]
step 5: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/css-filter.ts"}
tool read ok: import type {Theme, InversionFix} from '../definitions';
import {compareChromeVersions, chromiumVersion, isFirefox, firefoxVersion} from '../utils/platform';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns, isURLInList} from '../utils/url';

import {createTextStyle} from './text-style';
import {formatSitesFixesConfig} from './utils/format';
import {applyColorMatrix, createFilterMatrix} from './utils/matrix';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

declare const __CHROMIUM_MV2__: boolean;
declare const __CHROMIUM_MV3__: boolean;

export enum FilterMode {
    light = 0,
    dark = 1
}

/**
 * This checks if the current chromium version has the patch in it.
 * As of Chromium v81.0.4035.0 this has been the situation
 *
 * Bug report: https://bugs.chromium.org/p/chromium/issues/detail?id=501582
 * Patch: https://chromium-review.googlesource.com/c/chromium/src/+/1979258
 */
export function hasPatchForChromiumIssue501582(): boolean {
    return __CHROMIUM_MV3__ || Boolean(
        __CHROMIUM_MV2__ &&
        compareChromeVersions(chromiumVersion, '81.0.4035.0') >= 0
    );
}

/**
 * Since Firefox v102.0, they have changed to the new root behavior.
 * This was already the case for Chromium v81.0.4035.0 and Firefox now
 * switched over as well.
 */
export function hasFirefoxNewRootBehavior(): boolean {
    return Boolean(
        isFirefox &&
        compareChromeVersions(firefoxVersion, '102.0') >= 0
    );
}

export default function createCSSFilterStyleSheet(config: Theme, url: string, isTopFrame: boolean, fixes: string, index: SiteFixesIndex): string {
    const filterValue = getCSSFilterValue(config)!;
    const reverseFilterValue = 'invert(100%) hue-rotate(180deg)';
    return cssFilterStyleSheetTemplate('html', filterValue, reverseFilterValue, config, url, isTopFrame, fixes, index);
}

export function cssFilterStyleSheetTemplate(filterRoot: string, filterValue: string, reverseFilterValue: string, config: Theme, url: string, isTopFrame: boolean, fixes: string, index: SiteFixesIndex): string {
    const fix = getInversionFixesFor(url, fixes, index);

    const lines: string[] = [];

    lines.push('@media screen {');

    // Add leading rule
    if (filterValue && isTopFrame) {
        lines.push('');
        lines.push('/* Leading rule */');
        lines.push(createLeadingRule(filterRoot, filterValue));
    }

    if (config.mode === FilterMode.dark) {
        // Add reverse rule
        lines.push('');
        lines.push('/* Reverse rule */');
        lines.push(createReverseRule(reverseFilterValue, fix));
    }

    if (config.useFont || config.textStroke > 0) {
        // Add text rule
        lines.push('');
        lines.push('/* Font */');
        lines.push(createTextStyle(config));
    }

    // Full screen fix
    lines.push('');
    lines.push('/* Full screen */');
    [':-webkit-full-screen', ':-moz-full-screen', ':fullscreen'].forEach((fullScreen) => {
        lines.push(`${fullScreen}, ${fullScreen} * {`);
        lines.push('  -webkit-filter: none !important;');
        lines.push('  filter: none !important;');
        lines.push('}');
    });

    if (isTopFrame) {
        const light: [number, number, number] = [255, 255, 255];
        // If browser affected by Chromium Issue 501582, set dark background on html
        // Or if browser is Firefox v102+
        const bgColor = (!hasPatchForChromiumIssue501582() && !hasFirefoxNewRootBehavior()) && config.mode === FilterMode.dark ?
            applyColorMatrix(light, createFilterMatrix(config)).map(Math.round) :
            light;
        lines.push('');
        lines.push('/* Page background */');
        lines.push('html {');
        lines.push(`  background: rgb(${bgColor.join(',')}) !important;`);
        lines.push('}');
    }

    if (fix.css && fix.css.length > 0 && config.mode === FilterMode.dark) {
        lines.push('');
        lines.push('/* Custom rules */');
        lines.push(fix.css);
    }

    lines.push('');
    lines.push('}');

    return lines.join('\n');
}

export function getCSSFilterValue(config: Theme): string | null {
    const filters: string[] = [];

    if (config.mode === FilterMode.dark) {
        filters.push('invert(100%) hue-rotate(180deg)');
    }
    if (config.brightness !== 100) {
        filters.push(`brightness(${config.brightness}%)`);
    }
    if (config.contrast !== 100) {
        filters.push(`contrast(${config.contrast}%)`);
    }
    if (config.grayscale !== 0) {
        filters.push(`grayscale(${config.grayscale}%)`);
    }
    if (config.sepia !== 0) {
        filters.push(`sepia(${config.sepia}%)`);
    }

    if (filters.length === 0) {
        return null;
    }

    return filters.join(' ');
}

function createLeadingRule(filterRoot: string, filterValue: string): string {
    return [
        `${filterRoot} {`,
        `  -webkit-filter: ${filterValue} !important;`,
        `  filter: ${filterValue} !important;`,
        '}',
    ].join('\n');
}

function joinSelectors(selectors: string[]): string {
    return selectors.map((s) => s.replace(/\,$/, '')).join(',\n');
}

function createReverseRule(reverseFilterValue: string, fix: InversionFix): string {
    const lines: string[] = [];

    if (fix.invert.length > 0) {
        lines.push(`${joinSelectors(fix.invert)} {`);
        lines.push(`  -webkit-filter: ${reverseFilterValue} !important;`);
        lines.push(`  filter: ${reverseFilterValue} !important;`);
        lines.push('}');
    }

    if (fix.noinvert.length > 0) {
        lines.push(`${joinSelectors(fix.noinvert)} {`);
        lines.push('  -webkit-filter: none !important;');
        lines.push('  filter: none !important;');
        lines.push('}');
    }

    if (fix.removebg.length > 0) {
        lines.push(`${joinSelectors(fix.removebg)} {`);
        lines.push('  background: white !important;');
        lines.push('}');
    }

    return lines.join('\n');
}

/**
* Returns fixes for a given URL.
* If no matches found, common fixes will be returned.
* @param url Site URL.
* @param inversionFixes List of inversion fixes.
*/
export function getInversionFixesFor(url: string, fixes: string, index: SiteFixesIndex): InversionFix {
    const inversionFixes = getSitesFixesFor(url, fixes, index, parseInversionFixes);

    const common = {
        url: inversionFixes[0].url,
        invert: inversionFixes[0].invert || [],
        noinvert: inversionFixes[0].noinvert || [],
        removebg: inversionFixes[0].removebg || [],
        css: inversionFixes[0].css || '',
    };

    if (url) {
        // Search for match with given URL
        const matches = inversionFixes
            .slice(1)
            .filter((s) => isURLInList(url, s.url))
            .sort((a, b) => b.url[0].length - a.url[0].length);
        if (matches.length > 0) {
            const found = matches[0];
            return {
                url: found.url,
                invert: common.invert.concat(found.invert || []),
                noinvert: common.noinvert.concat(found.noinvert || []),
                removebg: common.removebg.concat(found.removebg || []),
                css: [common.css, found.css].filter((s) => s).join('\n'),
            };
        }
    }
    return common;
}

const inversionFixesCommands: { [key: string]: keyof InversionFix } = {
    'INVERT': 'invert',
    'NO INVERT': 'noinvert',
    'REMOVE BG': 'removebg',
    'CSS': 'css',
};

export function parseInversionFixes(text: string): InversionFix[] {
    return parseSitesFixesConfig<InversionFix>(text, {
        commands: Object.keys(inversionFixesCommands),
        getCommandPropName: (command) => inversionFixesCommands[command],
        parseCommandValue: (command, value) => {
            if (command === 'CSS') {
                return value.trim();
            }
            return parseArray(value);
        },
    });
}

export function formatInversionFixes(inversionFixes: InversionFix[]): string {
    const fixes = inversionFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(inversionFixesCommands),
        getPropCommandName: (prop) => Object.entries(inversionFixesCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (prop === 'css') {
                return (value as string).trim().replace(/\n+/g, '\n');
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'css') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

[evidence record sha256:32c79d52cd1054c48b733112cb469cf4fb26c7ebba7c76a60517e3a4f2b5d020 kind tool-call:read]
step 6: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/detector-hints.ts"}
tool read ok: import type {DetectorHint} from '../definitions';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {formatSitesFixesConfig} from './utils/format';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex, SitesFixesParserOptions} from './utils/parse';

const detectorHintsCommands: { [key: string]: keyof DetectorHint } = {
    'TARGET': 'target',
    'MATCH': 'match',
    'NO DARK THEME': 'noDarkTheme',
    'SYSTEM THEME': 'systemTheme',
    'IFRAME': 'iframe',
};

const detectorParserOptions: SitesFixesParserOptions<DetectorHint> = {
    commands: Object.keys(detectorHintsCommands),
    getCommandPropName: (command) => detectorHintsCommands[command],
    parseCommandValue: (command, value) => {
        if (command === 'TARGET') {
            return value.trim();
        }
        if (command === 'NO DARK THEME' || command === 'SYSTEM THEME') {
            return true;
        }
        return parseArray(value);
    },
};

export function parseDetectorHints(text: string): DetectorHint[] {
    return parseSitesFixesConfig<DetectorHint>(text, detectorParserOptions);
}

export function formatDetectorHints(detectorHints: DetectorHint[]): string {
    const fixes = detectorHints.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(detectorHintsCommands),
        getPropCommandName: (prop) => Object.entries(detectorHintsCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (Array.isArray(value)) {
                return formatArray(value).trim();
            }
            if (prop === 'noDarkTheme' || prop === 'systemTheme') {
                return '';
            }
            return String(value).trim();
        },
        shouldIgnoreProp: (_prop, value) => {
            return !value;
        },
    });
}

export function getDetectorHintsFor(url: string, text: string, index: SiteFixesIndex): DetectorHint[] | null {
    const fixes = getSitesFixesFor(url, text, index, parseDetectorHints);

    if (fixes.length === 0) {
        return null;
    }

    return fixes;
}

[evidence record sha256:c4053de75fa3dc27e6e552c1328f34ff6343f4dd1a37fb8e5226c3eeac1c16e7 kind tool-call:read]
step 7: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/format.ts"}
tool read ok: import {push} from '../../utils/array';

interface SiteFix {
    url: string[];
    [prop: string]: any;
}

interface SitesFixesFormatOptions {
    props: string[];
    getPropCommandName: (prop: string) => string;
    formatPropValue: (prop: string, value: string | string[]) => string;
    shouldIgnoreProp: (prop: string, value: string | string[]) => boolean;
}

export function formatSitesFixesConfig(fixes: SiteFix[], options: SitesFixesFormatOptions): string {
    const lines: string[] = [];

    fixes.forEach((fix, i) => {
        push(lines, fix.url);
        options.props.forEach((prop) => {
            const command = options.getPropCommandName(prop);
            const value = fix[prop];
            if (options.shouldIgnoreProp(prop, value)) {
                return;
            }
            lines.push('');
            lines.push(command);
            const formattedValue = options.formatPropValue(prop, value);
            if (formattedValue) {
                lines.push(formattedValue);
            }
        });
        if (i < fixes.length - 1) {
            lines.push('');
            lines.push('='.repeat(32));
            lines.push('');
        }
    });

    lines.push('');
    return lines.join('\n');
}

[evidence record sha256:80a24d9c658af983938f0391d84df6968148a5a6e0fb036f43bdbf40326cb5d4 kind tool-call:read]
step 8: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:94bff6c6025932d056561213e3a6e6019c92fc7437961d37a0bfcbdff1b33afb kind tool-call:read]
step 9: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:d1309c489d56dc7478c5f40f885d6c4b1e347c8db676c54baebdafc1ef887bab kind tool-call:read]
step 10: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:0e5a4268c9975b731b82037c7a5b7864fe666cc9530ed4b2fad3fc60f086bb05 kind tool-call:read]
step 11: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:ca16f635b86d9652e81a3af9811866c037f456a9673821ab9cfbf67447004c4a kind tool-call:read]
step 12: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:ed9e61ab29a622fb2fc0f33a23ef35a8d2a40073503fa17248eb6532ed1e7244 kind tool-call:read]
step 13: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:d720a132ef47e327daabbaeebe2ea81fa57794c119dce53fa869ec5d68fbdba1 kind tool-call:read]
step 14: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:24d4cb463a9e2eb7c5079c1fe51897bf1571207f765a3d1f242721c6f40a5b0d kind tool-call:read]
step 15: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:30503897ba36cfa714452c13c4e55d2d98b3ded05aba38657c7e95d4acd05d14 kind tool-call:read]
step 16: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:f33f8131aec5c14f454177a48d53de2ad7d2b78f6aa3d8a7bfd30b2ec05d3d0e kind tool-call:read]
step 17: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:4633bd0ac6d10c6fda50cce3ce6f22265ac4e42a0ae0bfe84c7d34d89e9fdc10 kind tool-call:read]
step 18: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:6112cfff98e43dcd7db6e1ed061b02ed46bda1a6e69adb798935fd6cb9f93371 kind tool-call:read]
step 19: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:3efb8c1d919b5ba58135b122f2c6faddcb6c13f400b2b4150914f04374b164f0 kind tool-call:read]
step 20: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:62e35a57e97ebb4244fb8c4be4e25254986a88b99d70e378ce35062efc924734 kind tool-call:read]
step 21: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:6456d808815a1fa3caac1981a0e8e71f83474416d93e5dc9783d3ff071e04e76 kind tool-call:read]
step 22: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:ed4917611dfc192ea45f8feda9d5066ed9edf72ceda8135cf95dbad179f2e1f5 kind tool-call:read]
step 23: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:b27f476646bb104ae46a98e87806f1aea9c0cf429223c6cc4a50d8bbf73a4274 kind tool-call:read]
step 24: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:c26b9a106dee686af4f3c5b072011d5680fb304e83cd0a7d85f17ebb8c2c20b1 kind tool-call:read]
step 25: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:eb386633a2182ff690d20f4582a4ed327d0f35860b405b743eca5cb24c3292cb kind tool-call:read]
step 26: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:952e01ece0725fd85a4e0f1c01e53770799aeb0affe63e7e37205a31a5acfd65 kind tool-call:read]
step 27: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:a7b1f902691a39ae3173fb9aa1d65ae0f1964eaf34a51c721b20b7706d04d8ee kind tool-call:read]
step 28: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:a4c3fb99c554c430d52d1ff4424a46ac2696b19fe7ebf0f5b636da58dced758f kind tool-call:read]
step 29: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:9964fa2e2adc0864ac4b023b17bf1fb1a7f9d4c1ccdc715be9e8082a7542547f kind tool-call:read]
step 30: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:9bcd39f4b0081f9c3f9b37d289fe5042a6c5715a3c82019f491d7acbd73b6875 kind tool-call:read]
step 31: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:25bfb38405f3455c0225d2bd3e4ee2d69fd3fe52b94afb96f88ebfb74aef3a3a kind tool-call:read]
step 32: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:ee2308994177ef56c3f9d882baa97a904371a6a614bdb8e91b86f066371192fe kind tool-call:read]
step 33: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:c46a34161cfca38f0bb204f25787d0898329b2c532b454ca3b476b48176a41b7 kind tool-call:read]
step 34: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:3e51fee2fecf08f5d40e6ac21a99d9a7d0522cb45514d411a7f4b751ae9fa66d kind tool-call:read]
stopped: max-tokens after 34 steps, 1042562 tokens
gate typecheck not-applicable: package.json declares no typecheck script [evidence record sha256:dcc2e6bb8b1f041c21c5dfd3819d32ebdcbd7bd319999a9fb62dca01576f5227]
gate lint passed: the command exited 0 [evidence record sha256:aef6139da142c0a0202be26697bec23ce8b453a1b74d7c3ace8ac70d8f6c2e3d]
gate format not-applicable: package.json declares no check-only format script, and running a writing formatter as a gate would edit the tree it is judging [evidence record sha256:de56ee4f49cd37e04578550df7589a9e701cb442e8b202888044c9c9e077f72e]
gate tests failed: the command exited 1 [evidence record sha256:09ca9c85efa000249154cf0f7414cfb5dee145ba5386d6d848d0bdc1f0360cd5]
gate file-set passed: nothing changed and no file set was declared, so there is nothing to check [evidence record sha256:283dfbb336f3dfb095eccbc9d64286d14f6856de062a0fbfd1097ddbb64772c1]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:6ab4a60c77fbee0a6e8337589bb53775332ffb0cc3347a678c83b90c1c1257c8]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:0d1bddaaa0335ae01c8ded7b8c8df77a28e4d5968164123819f75b8963b3a344]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:e4544917a11298f6a754745d2738fe0f6523c4e67aad3f27582878f46f5e3ebd]
gate diff-budget passed (advisory): within budget: 0 file(s) and 0 added line(s) [evidence record sha256:e6cc394dbdfbda0b24d095bc7c9b221e9009e6f31b3a42113876248f0ffd3af6]
ratchet accepted attempt 1: the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage) [evidence record sha256:b21839bfd4adb672488d32efb6cff071affa3cde50bf7afa6bcf5ba1cf556945]
auto-resolve attempt 2 of 2
step 1: calling local:qwen3.8:27b
tool read <- {"path":"tests/unit/config/config.tests.ts"}
tool read ok: import {readFile} from 'node:fs';

import type {StaticTheme} from '../../../src/definitions';
import {parseInversionFixes, formatInversionFixes} from '../../../src/generators/css-filter';
import {parseDetectorHints, formatDetectorHints} from '../../../src/generators/detector-hints';
import {parseDynamicThemeFixes, formatDynamicThemeFixes} from '../../../src/generators/dynamic-theme';
import {parseStaticThemes, formatStaticThemes} from '../../../src/generators/static-theme';
import {parseColorSchemeConfig} from '../../../src/utils/colorscheme-parser';
import type {ParsedColorSchemeConfig} from '../../../src/utils/colorscheme-parser';
import {parseArray, formatArray, getTextDiffIndex, getTextPositionMessage} from '../../../src/utils/text';
import {compareURLPatterns} from '../../../src/utils/url';
import {rootPath} from '../../support/test-utils';

function readConfig(fileName: string) {
    return new Promise<string>((resolve, reject) => {
        readFile(rootPath('src/config', fileName), {encoding: 'utf-8'}, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            resolve(data);
        });
    });
}

function isURLPatternValid(url: string) {
    return url.length > 0 && url.indexOf('://') < 0;
}

function throwIfDifferent(input: string, expected: string, message: string) {
    return () => {
        const diffIndex = getTextDiffIndex(input, expected);
        if (diffIndex >= 0) {
            throw new Error(`${message}\n${getTextPositionMessage(input, diffIndex)}`);
        }
    };
}

function formatColorSchemeConfig(scheme: ParsedColorSchemeConfig): string {
    const names = Object.keys(scheme.dark);
    const lines = [];
    for (const name of names) {
        lines.push(name);
        lines.push('');
        for (const color of ['dark', 'light']) {
            const style = scheme[color as keyof ParsedColorSchemeConfig][name];
            if (style) {
                const {backgroundColor, textColor} = style;
                lines.push(color.toUpperCase());
                if (backgroundColor) {
                    lines.push(`background: ${backgroundColor.toLowerCase()}`);
                }
                if (textColor) {
                    lines.push(`text: ${textColor.toLowerCase()}`);
                }
                lines.push('');
            }
        }
        lines.push('='.repeat(32));
        lines.push('');
    }
    lines.pop();
    lines.pop();
    return lines.join('\n');
}

test('Dark Sites list', async () => {
    const file = await readConfig('dark-sites.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const sites = parseArray(file);

    // is not empty
    expect(sites.length).toBeGreaterThan(0);

    // url patterns should have no protocol
    expect(sites.every(isURLPatternValid)).toBe(true);

    // sites are sorted alphabetically
    expect(sites.slice().sort(compareURLPatterns)).toEqual(sites);

    // sites are properly formatted
    expect(throwIfDifferent(file, formatArray(sites), 'Dark Sites list format error')).not.toThrow();
});

test('Detector Hints config', async () => {
    const file = await readConfig('detector-hints.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const hints = parseDetectorHints(file);

    // each hint has valid URL
    expect(hints.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // hints are sorted alphabetically
    expect(hints.map(({url}) => url[0])).toEqual(hints.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    expect(hints.every(({target, match}) => ![target].concat(match).some((s) => commaSelector.test(s)))).toBe(true);

    // only a single selector is allowed for target
    expect(hints.every(({target, noDarkTheme, systemTheme}) => noDarkTheme || systemTheme || typeof target === 'string' && !target.includes('\n'))).toBe(true);

    // hints are properly formatted
    expect(throwIfDifferent(file, formatDetectorHints(hints), 'Detector Hints format error')).not.toThrow();

    // should parse empty config
    expect(parseDetectorHints('')).toEqual([]);

    // should skip unsupported commands
    expect(parseDetectorHints([
        'inbox.google.com',
        'mail.google.com',
        'TARGET', 'a',
        'MATCH', '.b', '#c',
        'UNSUPPORTED', 'c',
        '========',
        'proton.me',
        'SYSTEM THEME',
        '========',
        'twitter.com',
        'UNSUPPORTED', 'a', 'b',
        'TARGET', 'c',
        'MATCH', '[d="e"]',
        '========',
        'wikipedia.org',
        'NO DARK THEME',
    ].join('\n'))).toEqual([
        {url: ['inbox.google.com', 'mail.google.com'], target: 'a', match: ['.b', '#c']},
        {url: ['proton.me'], systemTheme: true},
        {url: ['twitter.com'], target: 'c', match: ['[d="e"]']},
        {url: ['wikipedia.org'], noDarkTheme: true},
    ] as any);
});

test('Dynamic Theme Fixes config', async () => {
    const file = await readConfig('dynamic-theme-fixes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const fixes = parseDynamicThemeFixes(file);

    // there is a common fix
    expect(fixes[0].url[0]).toEqual('*');

    // each fix has valid URL
    expect(fixes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // fixes are sorted alphabetically
    expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    const commaSelector = /\,(?![^\(|\"]*(\)|\"))/;
    expect(fixes.every(({invert, ignoreInlineStyle, ignoreImageAnalysis}) => (invert || []).concat(ignoreInlineStyle || []).concat(ignoreImageAnalysis || []).every((s) => !commaSelector.test(s)))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatDynamicThemeFixes(fixes), 'Dynamic fixes format error')).not.toThrow();

    // should parse empty config
    expect(parseDynamicThemeFixes('')).toEqual([]);

    // should skip unsupported commands
    expect(parseDynamicThemeFixes([
        'inbox.google.com',
        'mail.google.com',
        'INVERT', 'a', 'b',
        'CSS', '.x { color: white !important; }',
        'UNSUPPORTED', 'c', 'd',
        '========',
        'twitter.com',
        'UNSUPPORTED', 'a', 'b',
        'INVERT', 'c', 'd',
        '========',
        'wikipedia.org',
        'IGNORE INLINE STYLE', 'a', 'b',
        '========',
        'duckduckgo.com',
        'IGNORE IMAGE ANALYSIS', 'img[alt="Logo"]', 'canvas',
    ].join('\n'))).toEqual([
        {url: ['inbox.google.com', 'mail.google.com'], invert: ['a', 'b'], css: '.x { color: white !important; }'},
        {url: ['twitter.com'], invert: ['c', 'd']},
        {url: ['wikipedia.org'], ignoreInlineStyle: ['a', 'b']},
        {url: ['duckduckgo.com'], ignoreImageAnalysis: ['img[alt="Logo"]', 'canvas']},
    ] as any);
});

test('Inversion Fixes config', async () => {
    const file = await readConfig('inversion-fixes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const fixes = parseInversionFixes(file);

    // there is a common fix
    expect(fixes[0].url[0]).toEqual('*');

    // each fix has valid URL
    expect(fixes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // fixes are sorted alphabetically
    expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    expect(fixes.every(({invert, noinvert, removebg}) => (invert || []).concat(noinvert || []).concat(removebg || []).every((s) => s.indexOf(',') < 0))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatInversionFixes(fixes), 'Inversion fixes format error')).not.toThrow();
});

test('Static Themes config', async () => {
    const file = await readConfig('static-themes.config');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const themes = parseStaticThemes(file);

    // there is a common theme
    expect(themes[0].url[0]).toEqual('*');

    // each theme has valid URL
    expect(themes.every(({url}) => url.every(isURLPatternValid))).toBe(true);

    // themes are sorted alphabetically
    expect(themes.map(({url}) => url[0])).toEqual(themes.map(({url}) => url[0]).sort(compareURLPatterns));

    // selectors should have no comma
    expect(themes.every((t) => (Object.keys(t) as Array<keyof StaticTheme>)
        .filter((prop) => ['url', 'noCommon'].indexOf(prop) < 0)
        .every((prop) => (t[prop] as string[])
            .every((s) => s.indexOf(',') < 0)))).toBe(true);

    // fixes are properly formatted
    expect(throwIfDifferent(file, formatStaticThemes(themes), 'Static theme format error')).not.toThrow();
});

test('Colorscheme config', async () => {
    const file = await readConfig('color-schemes.drconf');

    // there is no \r character
    expect(file.indexOf('\r')).toEqual(-1);

    // there are no trailing spaces
    expect(file.indexOf(' \n')).toEqual(-1);

    const {result: schemes, error} = parseColorSchemeConfig(file);

    // Their is no error
    expect(error).toBeNull();

    // There is a default Dark color scheme
    expect(schemes.dark['Default']).toBeDefined();

    // There is a default Light color scheme
    expect(schemes.light['Default']).toBeDefined();

    // Check formatting
    expect(formatColorSchemeConfig(schemes)).toEqual(file);
});

[evidence record sha256:2e1e9082185cb63945b842d1a1284a4d52496dc01f3a578b23737f411a02b143 kind tool-call:read]
tool list <- {}
tool list ok: .git/
.gitattributes
.github/
.gitignore
.npmignore
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
README.md
SECURITY.md
docs/
eslint-plugin-local.js
eslint.config.js
index.d.ts
integrity/
node_modules/
package-lock.json
package.json
src/
tasks/
tests/
[evidence record sha256:fe1c818769df8433d498d79976549333e8e24445baec6c97f48a9d81912f5325 kind tool-call:list]
step 2: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/dynamic-theme.ts"}
tool read ok: import type {DynamicThemeFix} from '../definitions';
import {formatCSS} from '../utils/css-text/format-css';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {formatSitesFixesConfig} from './utils/format';
import {parseSitesFixesConfig, getSitesFixesFor, getDomain} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

declare const __CHROMIUM_MV2__: boolean;
declare const __CHROMIUM_MV3__: boolean;

const dynamicThemeFixesCommands: { [key: string]: keyof DynamicThemeFix } = {
    'INVERT': 'invert',
    'CSS': 'css',
    'IGNORE INLINE STYLE': 'ignoreInlineStyle',
    'IGNORE IMAGE ANALYSIS': 'ignoreImageAnalysis',
    'IGNORE CSS URL': 'ignoreCSSUrl',
};

export function parseDynamicThemeFixes(text: string): DynamicThemeFix[] {
    return parseSitesFixesConfig<DynamicThemeFix>(text, {
        commands: Object.keys(dynamicThemeFixesCommands),
        getCommandPropName: (command) => dynamicThemeFixesCommands[command],
        parseCommandValue: (command, value) => {
            if (command !== 'CSS') {
                return value.trim();
            }
            return parseArray(value);
        },
    });
}

export function formatDynamicThemeFixes(dynamicThemeFixes: DynamicThemeFix[]): string {
    const fixes = dynamicThemeFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(dynamicThemeFixesCommands),
        getPropCommandName: (prop) => Object.entries(dynamicThemeFixesCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (prop === 'css') {
                return formatCSS(value as string);
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'css') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

export function getDynamicThemeFixesFor(url: string, text: string, index: SiteFixesIndex, enabledForPDF: boolean): DynamicThemeFix[] | null {
    const fixes = getSitesFixesFor(url, text, index, parseDynamicThemeFixes);

    if (fixes.length === 0 || fixes[0].url[0] !== '*') {
        return null;
    }

    if (enabledForPDF) {
        // Copy part of fixes which will be mutated
        const invert = fixes[0].invert?.slice();
        const commonFix = {...fixes[0], invert};
        const pdfFixes: DynamicThemeFix[] = [
            commonFix,
            ...fixes.slice(1),
        ];

        const inversionFix = __CHROMIUM_MV2__ || __CHROMIUM_MV3__ ?
            '\nembed[type="application/pdf"][src="about:blank"] { filter: invert(100%) contrast(90%); }' :
            '\nembed[type="application/pdf"] { filter: invert(100%) contrast(90%); }';
        if (!commonFix.css.endsWith(inversionFix)) {
            commonFix.css += inversionFix;
        }

        if (['drive.google.com', 'mail.google.com'].includes(getDomain(url))) {
            const nestedInversionFix = 'div[role="dialog"] div[role="document"]';
            if (commonFix.invert.at(-1) !== nestedInversionFix) {
                commonFix.invert.push(nestedInversionFix);
            }
        }

        return pdfFixes;
    }

    return fixes;
}

[evidence record sha256:7cfc0b4a743369a48f3e57d827e42ff6f7bd98f7308411a3365178a42e3095bb kind tool-call:read]
step 3: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:c3a849021821f9ea408913ced1da7ba0fe85311646dda9ef5d233b9797542eb4 kind tool-call:read]
step 4: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/static-theme.ts"}
tool read ok: import type {Theme, StaticTheme} from '../definitions';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {createTextStyle} from './text-style';
import {formatSitesFixesConfig} from './utils/format';
import {applyColorMatrix, createFilterMatrix} from './utils/matrix';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

interface ThemeColors {
    [prop: string]: number[];
    neutralBg: number[];
    neutralText: number[];
    redBg: number[];
    redText: number[];
    greenBg: number[];
    greenText: number[];
    blueBg: number[];
    blueText: number[];
    fadeBg: number[];
    fadeText: number[];
}

const darkTheme: ThemeColors = {
    neutralBg: [16, 20, 23],
    neutralText: [167, 158, 139],
    redBg: [64, 12, 32],
    redText: [247, 142, 102],
    greenBg: [32, 64, 48],
    greenText: [128, 204, 148],
    blueBg: [32, 48, 64],
    blueText: [128, 182, 204],
    fadeBg: [16, 20, 23, 0.5],
    fadeText: [167, 158, 139, 0.5],
};

const lightTheme: ThemeColors = {
    neutralBg: [255, 242, 228],
    neutralText: [0, 0, 0],
    redBg: [255, 85, 170],
    redText: [140, 14, 48],
    greenBg: [192, 255, 170],
    greenText: [0, 128, 0],
    blueBg: [173, 215, 229],
    blueText: [28, 16, 171],
    fadeBg: [0, 0, 0, 0.5],
    fadeText: [0, 0, 0, 0.5],
};

function rgb([r, g, b, a]: number[]): string {
    if (typeof a === 'number') {
        return `rgba(${r}, ${g}, ${b}, ${a})`;
    }
    return `rgb(${r}, ${g}, ${b})`;
}

function mix(color1: number[], color2: number[], t: number): number[] {
    return color1.map((c, i) => Math.round(c * (1 - t) + color2[i] * t));
}

export default function createStaticStylesheet(config: Theme, url: string, isTopFrame: boolean, staticThemes: string, staticThemesIndex: SiteFixesIndex): string {
    const srcTheme = config.mode === 1 ? darkTheme : lightTheme;
    const theme = Object.entries(srcTheme).reduce((t, [prop, color]) => {
        const [r, g, b, a] = color;
        t[prop] = applyColorMatrix([r, g, b], createFilterMatrix({...config, mode: 0}));
        if (a !== undefined) {
            t[prop].push(a);
        }
        return t;
    }, {} as ThemeColors);

    const themes = getSitesFixesFor(url, staticThemes, staticThemesIndex, parseStaticThemes);

    const commonTheme = themes.find((t) => t.url[0] === '*');
    const siteTheme = themes.find((t) => t.url[0] !== '*');

    if (!commonTheme) {
        return '';
    }

    const lines: string[] = [];

    if (!siteTheme || !siteTheme.noCommon) {
        lines.push('/* Common theme */');
        lines.push(...ruleGenerators.map((gen) => gen(commonTheme, theme)!));
    }

    if (siteTheme) {
        lines.push(`/* Theme for ${siteTheme.url.join(' ')} */`);
        lines.push(...ruleGenerators.map((gen) => gen(siteTheme, theme)!));
    }

    if (config.useFont || config.textStroke > 0) {
        lines.push('/* Font */');
        lines.push(createTextStyle(config));
    }

    return lines
        .filter((ln) => ln)
        .join('\n');
}

function createRuleGen(getSelectors: (siteTheme: StaticTheme) => string[] | undefined, generateDeclarations: (theme: ThemeColors) => string[], modifySelector: ((s: string) => string) = (s) => s) {
    return (siteTheme: StaticTheme, themeColors: ThemeColors) => {
        const selectors = getSelectors(siteTheme);
        if (selectors == null || selectors.length === 0) {
            return null;
        }
        const lines: string[] = [];
        selectors.forEach((s, i) => {
            let ln = modifySelector(s);
            if (i < selectors.length - 1) {
                ln += ',';
            } else {
                ln += ' {';
            }
            lines.push(ln);
        });
        const declarations = generateDeclarations(themeColors);
        declarations.forEach((d) => lines.push(`    ${d} !important;`));
        lines.push('}');
        return lines.join('\n');
    };
}

const mx = {
    bg: {
        hover: 0.075,
        active: 0.1,
    },
    fg: {
        hover: 0.25,
        active: 0.5,
    },
    border: 0.5,
};

const ruleGenerators = [
    createRuleGen((t) => t.neutralBg, (t) => [`background-color: ${rgb(t.neutralBg)}`]),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(t.neutralBg)}`]),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(mix(t.neutralBg, [255, 255, 255], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.neutralBgActive, (t) => [`background-color: ${rgb(mix(t.neutralBg, [255, 255, 255], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.neutralText, (t) => [`color: ${rgb(t.neutralText)}`]),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(t.neutralText)}`]),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(mix(t.neutralText, [255, 255, 255], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.neutralTextActive, (t) => [`color: ${rgb(mix(t.neutralText, [255, 255, 255], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.neutralBorder, (t) => [`border-color: ${rgb(mix(t.neutralBg, t.neutralText, mx.border))}`]),

    createRuleGen((t) => t.redBg, (t) => [`background-color: ${rgb(t.redBg)}`]),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(t.redBg)}`]),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(mix(t.redBg, [255, 0, 64], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.redBgActive, (t) => [`background-color: ${rgb(mix(t.redBg, [255, 0, 64], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.redText, (t) => [`color: ${rgb(t.redText)}`]),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(t.redText)}`]),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.redTextActive, (t) => [`color: ${rgb(mix(t.redText, [255, 255, 0], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.redBorder, (t) => [`border-color: ${rgb(mix(t.redBg, t.redText, mx.border))}`]),

    createRuleGen((t) => t.greenBg, (t) => [`background-color: ${rgb(t.greenBg)}`]),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(t.greenBg)}`]),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(mix(t.greenBg, [128, 255, 182], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.greenBgActive, (t) => [`background-color: ${rgb(mix(t.greenBg, [128, 255, 182], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.greenText, (t) => [`color: ${rgb(t.greenText)}`]),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(t.greenText)}`]),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.greenTextActive, (t) => [`color: ${rgb(mix(t.greenText, [182, 255, 224], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.greenBorder, (t) => [`border-color: ${rgb(mix(t.greenBg, t.greenText, mx.border))}`]),

    createRuleGen((t) => t.blueBg, (t) => [`background-color: ${rgb(t.blueBg)}`]),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(t.blueBg)}`]),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(mix(t.blueBg, [0, 128, 255], mx.bg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.blueBgActive, (t) => [`background-color: ${rgb(mix(t.blueBg, [0, 128, 255], mx.bg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.blueText, (t) => [`color: ${rgb(t.blueText)}`]),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(t.blueText)}`]),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.hover))}`], (s) => `${s}:hover`),
    createRuleGen((t) => t.blueTextActive, (t) => [`color: ${rgb(mix(t.blueText, [182, 224, 255], mx.fg.active))}`], (s) => `${s}:active, ${s}:focus`),
    createRuleGen((t) => t.blueBorder, (t) => [`border-color: ${rgb(mix(t.blueBg, t.blueText, mx.border))}`]),

    createRuleGen((t) => t.fadeBg, (t) => [`background-color: ${rgb(t.fadeBg)}`]),
    createRuleGen((t) => t.fadeText, (t) => [`color: ${rgb(t.fadeText)}`]),
    createRuleGen((t) => t.transparentBg, () => ['background-color: transparent']),
    createRuleGen((t) => t.noImage, () => ['background-image: none']),
    createRuleGen((t) => t.invert, () => ['filter: invert(100%) hue-rotate(180deg)']),
];

const staticThemeCommands: { [key: string]: keyof StaticTheme } = {
    'NO COMMON': 'noCommon',

    'NEUTRAL BG': 'neutralBg',
    'NEUTRAL BG ACTIVE': 'neutralBgActive',
    'NEUTRAL TEXT': 'neutralText',
    'NEUTRAL TEXT ACTIVE': 'neutralTextActive',
    'NEUTRAL BORDER': 'neutralBorder',

    'RED BG': 'redBg',
    'RED BG ACTIVE': 'redBgActive',
    'RED TEXT': 'redText',
    'RED TEXT ACTIVE': 'redTextActive',
    'RED BORDER': 'redBorder',

    'GREEN BG': 'greenBg',
    'GREEN BG ACTIVE': 'greenBgActive',
    'GREEN TEXT': 'greenText',
    'GREEN TEXT ACTIVE': 'greenTextActive',
    'GREEN BORDER': 'greenBorder',

    'BLUE BG': 'blueBg',
    'BLUE BG ACTIVE': 'blueBgActive',
    'BLUE TEXT': 'blueText',
    'BLUE TEXT ACTIVE': 'blueTextActive',
    'BLUE BORDER': 'blueBorder',

    'FADE BG': 'fadeBg',
    'FADE TEXT': 'fadeText',
    'TRANSPARENT BG': 'transparentBg',

    'NO IMAGE': 'noImage',
    'INVERT': 'invert',
};

export function parseStaticThemes($themes: string): StaticTheme[] {
    return parseSitesFixesConfig<StaticTheme>($themes, {
        commands: Object.keys(staticThemeCommands),
        getCommandPropName: (command) => staticThemeCommands[command],
        parseCommandValue: (command, value) => {
            if (command === 'NO COMMON') {
                return true;
            }
            return parseArray(value);
        },
    });
}

function camelCaseToUpperCase(text: string): string {
    return text.replace(/([a-z])([A-Z])/g, '$1 $2').toUpperCase();
}

export function formatStaticThemes(staticThemes: StaticTheme[]): string {
    const themes = staticThemes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(themes, {
        props: Object.values(staticThemeCommands),
        getPropCommandName: camelCaseToUpperCase,
        formatPropValue: (prop, value) => {
            if (prop === 'noCommon') {
                return '';
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'noCommon') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

[evidence record sha256:a033d3cfbc5cb8dd8672d1a018868868e385a169de4a0e10dbc1fbeafdf72cb1 kind tool-call:read]
step 5: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/css-filter.ts"}
tool read ok: import type {Theme, InversionFix} from '../definitions';
import {compareChromeVersions, chromiumVersion, isFirefox, firefoxVersion} from '../utils/platform';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns, isURLInList} from '../utils/url';

import {createTextStyle} from './text-style';
import {formatSitesFixesConfig} from './utils/format';
import {applyColorMatrix, createFilterMatrix} from './utils/matrix';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex} from './utils/parse';

declare const __CHROMIUM_MV2__: boolean;
declare const __CHROMIUM_MV3__: boolean;

export enum FilterMode {
    light = 0,
    dark = 1
}

/**
 * This checks if the current chromium version has the patch in it.
 * As of Chromium v81.0.4035.0 this has been the situation
 *
 * Bug report: https://bugs.chromium.org/p/chromium/issues/detail?id=501582
 * Patch: https://chromium-review.googlesource.com/c/chromium/src/+/1979258
 */
export function hasPatchForChromiumIssue501582(): boolean {
    return __CHROMIUM_MV3__ || Boolean(
        __CHROMIUM_MV2__ &&
        compareChromeVersions(chromiumVersion, '81.0.4035.0') >= 0
    );
}

/**
 * Since Firefox v102.0, they have changed to the new root behavior.
 * This was already the case for Chromium v81.0.4035.0 and Firefox now
 * switched over as well.
 */
export function hasFirefoxNewRootBehavior(): boolean {
    return Boolean(
        isFirefox &&
        compareChromeVersions(firefoxVersion, '102.0') >= 0
    );
}

export default function createCSSFilterStyleSheet(config: Theme, url: string, isTopFrame: boolean, fixes: string, index: SiteFixesIndex): string {
    const filterValue = getCSSFilterValue(config)!;
    const reverseFilterValue = 'invert(100%) hue-rotate(180deg)';
    return cssFilterStyleSheetTemplate('html', filterValue, reverseFilterValue, config, url, isTopFrame, fixes, index);
}

export function cssFilterStyleSheetTemplate(filterRoot: string, filterValue: string, reverseFilterValue: string, config: Theme, url: string, isTopFrame: boolean, fixes: string, index: SiteFixesIndex): string {
    const fix = getInversionFixesFor(url, fixes, index);

    const lines: string[] = [];

    lines.push('@media screen {');

    // Add leading rule
    if (filterValue && isTopFrame) {
        lines.push('');
        lines.push('/* Leading rule */');
        lines.push(createLeadingRule(filterRoot, filterValue));
    }

    if (config.mode === FilterMode.dark) {
        // Add reverse rule
        lines.push('');
        lines.push('/* Reverse rule */');
        lines.push(createReverseRule(reverseFilterValue, fix));
    }

    if (config.useFont || config.textStroke > 0) {
        // Add text rule
        lines.push('');
        lines.push('/* Font */');
        lines.push(createTextStyle(config));
    }

    // Full screen fix
    lines.push('');
    lines.push('/* Full screen */');
    [':-webkit-full-screen', ':-moz-full-screen', ':fullscreen'].forEach((fullScreen) => {
        lines.push(`${fullScreen}, ${fullScreen} * {`);
        lines.push('  -webkit-filter: none !important;');
        lines.push('  filter: none !important;');
        lines.push('}');
    });

    if (isTopFrame) {
        const light: [number, number, number] = [255, 255, 255];
        // If browser affected by Chromium Issue 501582, set dark background on html
        // Or if browser is Firefox v102+
        const bgColor = (!hasPatchForChromiumIssue501582() && !hasFirefoxNewRootBehavior()) && config.mode === FilterMode.dark ?
            applyColorMatrix(light, createFilterMatrix(config)).map(Math.round) :
            light;
        lines.push('');
        lines.push('/* Page background */');
        lines.push('html {');
        lines.push(`  background: rgb(${bgColor.join(',')}) !important;`);
        lines.push('}');
    }

    if (fix.css && fix.css.length > 0 && config.mode === FilterMode.dark) {
        lines.push('');
        lines.push('/* Custom rules */');
        lines.push(fix.css);
    }

    lines.push('');
    lines.push('}');

    return lines.join('\n');
}

export function getCSSFilterValue(config: Theme): string | null {
    const filters: string[] = [];

    if (config.mode === FilterMode.dark) {
        filters.push('invert(100%) hue-rotate(180deg)');
    }
    if (config.brightness !== 100) {
        filters.push(`brightness(${config.brightness}%)`);
    }
    if (config.contrast !== 100) {
        filters.push(`contrast(${config.contrast}%)`);
    }
    if (config.grayscale !== 0) {
        filters.push(`grayscale(${config.grayscale}%)`);
    }
    if (config.sepia !== 0) {
        filters.push(`sepia(${config.sepia}%)`);
    }

    if (filters.length === 0) {
        return null;
    }

    return filters.join(' ');
}

function createLeadingRule(filterRoot: string, filterValue: string): string {
    return [
        `${filterRoot} {`,
        `  -webkit-filter: ${filterValue} !important;`,
        `  filter: ${filterValue} !important;`,
        '}',
    ].join('\n');
}

function joinSelectors(selectors: string[]): string {
    return selectors.map((s) => s.replace(/\,$/, '')).join(',\n');
}

function createReverseRule(reverseFilterValue: string, fix: InversionFix): string {
    const lines: string[] = [];

    if (fix.invert.length > 0) {
        lines.push(`${joinSelectors(fix.invert)} {`);
        lines.push(`  -webkit-filter: ${reverseFilterValue} !important;`);
        lines.push(`  filter: ${reverseFilterValue} !important;`);
        lines.push('}');
    }

    if (fix.noinvert.length > 0) {
        lines.push(`${joinSelectors(fix.noinvert)} {`);
        lines.push('  -webkit-filter: none !important;');
        lines.push('  filter: none !important;');
        lines.push('}');
    }

    if (fix.removebg.length > 0) {
        lines.push(`${joinSelectors(fix.removebg)} {`);
        lines.push('  background: white !important;');
        lines.push('}');
    }

    return lines.join('\n');
}

/**
* Returns fixes for a given URL.
* If no matches found, common fixes will be returned.
* @param url Site URL.
* @param inversionFixes List of inversion fixes.
*/
export function getInversionFixesFor(url: string, fixes: string, index: SiteFixesIndex): InversionFix {
    const inversionFixes = getSitesFixesFor(url, fixes, index, parseInversionFixes);

    const common = {
        url: inversionFixes[0].url,
        invert: inversionFixes[0].invert || [],
        noinvert: inversionFixes[0].noinvert || [],
        removebg: inversionFixes[0].removebg || [],
        css: inversionFixes[0].css || '',
    };

    if (url) {
        // Search for match with given URL
        const matches = inversionFixes
            .slice(1)
            .filter((s) => isURLInList(url, s.url))
            .sort((a, b) => b.url[0].length - a.url[0].length);
        if (matches.length > 0) {
            const found = matches[0];
            return {
                url: found.url,
                invert: common.invert.concat(found.invert || []),
                noinvert: common.noinvert.concat(found.noinvert || []),
                removebg: common.removebg.concat(found.removebg || []),
                css: [common.css, found.css].filter((s) => s).join('\n'),
            };
        }
    }
    return common;
}

const inversionFixesCommands: { [key: string]: keyof InversionFix } = {
    'INVERT': 'invert',
    'NO INVERT': 'noinvert',
    'REMOVE BG': 'removebg',
    'CSS': 'css',
};

export function parseInversionFixes(text: string): InversionFix[] {
    return parseSitesFixesConfig<InversionFix>(text, {
        commands: Object.keys(inversionFixesCommands),
        getCommandPropName: (command) => inversionFixesCommands[command],
        parseCommandValue: (command, value) => {
            if (command === 'CSS') {
                return value.trim();
            }
            return parseArray(value);
        },
    });
}

export function formatInversionFixes(inversionFixes: InversionFix[]): string {
    const fixes = inversionFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(inversionFixesCommands),
        getPropCommandName: (prop) => Object.entries(inversionFixesCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (prop === 'css') {
                return (value as string).trim().replace(/\n+/g, '\n');
            }
            return formatArray(value as string[]).trim();
        },
        shouldIgnoreProp: (prop, value) => {
            if (prop === 'css') {
                return !value;
            }
            return !(Array.isArray(value) && value.length > 0);
        },
    });
}

[evidence record sha256:e9ccfe772f098032f4ae79336e05b4b813ae36cdb9509c39b1721d386581d692 kind tool-call:read]
step 6: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/detector-hints.ts"}
tool read ok: import type {DetectorHint} from '../definitions';
import {parseArray, formatArray} from '../utils/text';
import {compareURLPatterns} from '../utils/url';

import {formatSitesFixesConfig} from './utils/format';
import {parseSitesFixesConfig, getSitesFixesFor} from './utils/parse';
import type {SiteFixesIndex, SitesFixesParserOptions} from './utils/parse';

const detectorHintsCommands: { [key: string]: keyof DetectorHint } = {
    'TARGET': 'target',
    'MATCH': 'match',
    'NO DARK THEME': 'noDarkTheme',
    'SYSTEM THEME': 'systemTheme',
    'IFRAME': 'iframe',
};

const detectorParserOptions: SitesFixesParserOptions<DetectorHint> = {
    commands: Object.keys(detectorHintsCommands),
    getCommandPropName: (command) => detectorHintsCommands[command],
    parseCommandValue: (command, value) => {
        if (command === 'TARGET') {
            return value.trim();
        }
        if (command === 'NO DARK THEME' || command === 'SYSTEM THEME') {
            return true;
        }
        return parseArray(value);
    },
};

export function parseDetectorHints(text: string): DetectorHint[] {
    return parseSitesFixesConfig<DetectorHint>(text, detectorParserOptions);
}

export function formatDetectorHints(detectorHints: DetectorHint[]): string {
    const fixes = detectorHints.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0]));

    return formatSitesFixesConfig(fixes, {
        props: Object.values(detectorHintsCommands),
        getPropCommandName: (prop) => Object.entries(detectorHintsCommands).find(([, p]) => p === prop)![0],
        formatPropValue: (prop, value) => {
            if (Array.isArray(value)) {
                return formatArray(value).trim();
            }
            if (prop === 'noDarkTheme' || prop === 'systemTheme') {
                return '';
            }
            return String(value).trim();
        },
        shouldIgnoreProp: (_prop, value) => {
            return !value;
        },
    });
}

export function getDetectorHintsFor(url: string, text: string, index: SiteFixesIndex): DetectorHint[] | null {
    const fixes = getSitesFixesFor(url, text, index, parseDetectorHints);

    if (fixes.length === 0) {
        return null;
    }

    return fixes;
}

[evidence record sha256:28a51ecd25d503b76f94f769961b00d527da3841b1ff1cb12e30b435dc72efe7 kind tool-call:read]
step 7: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/format.ts"}
tool read ok: import {push} from '../../utils/array';

interface SiteFix {
    url: string[];
    [prop: string]: any;
}

interface SitesFixesFormatOptions {
    props: string[];
    getPropCommandName: (prop: string) => string;
    formatPropValue: (prop: string, value: string | string[]) => string;
    shouldIgnoreProp: (prop: string, value: string | string[]) => boolean;
}

export function formatSitesFixesConfig(fixes: SiteFix[], options: SitesFixesFormatOptions): string {
    const lines: string[] = [];

    fixes.forEach((fix, i) => {
        push(lines, fix.url);
        options.props.forEach((prop) => {
            const command = options.getPropCommandName(prop);
            const value = fix[prop];
            if (options.shouldIgnoreProp(prop, value)) {
                return;
            }
            lines.push('');
            lines.push(command);
            const formattedValue = options.formatPropValue(prop, value);
            if (formattedValue) {
                lines.push(formattedValue);
            }
        });
        if (i < fixes.length - 1) {
            lines.push('');
            lines.push('='.repeat(32));
            lines.push('');
        }
    });

    lines.push('');
    return lines.join('\n');
}

[evidence record sha256:ed7d62955c4bf85f8f8a11f4bf7ee341a361151f252f5099eb86bf5022317910 kind tool-call:read]
step 8: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:9b4da20907de2c979b4473785b7d22aa266583d8d3154b77b35b00b3b289566a kind tool-call:read]
step 9: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:29d27c47927d0ca5a3f6b58bd66130a5097706004d4c82e6a2ad936eb3bc18e8 kind tool-call:read]
step 10: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:de668d87df141516420b0362a9a7db8d31bc860a0e7ace5da094cc4651e0d810 kind tool-call:read]
step 11: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:416776f3f3eeca003625e597525a38dc13c1bdd2c4184098d2ec80e83007abd6 kind tool-call:read]
step 12: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:0fe0276554a87070e155c415feb1a06d057a7a3dad01f877feec84f29eaf75c4 kind tool-call:read]
step 13: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:9448bd15a66a999e7f087e3886b10ead52edb80e0530a0fddfd089eab2318642 kind tool-call:read]
step 14: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:0d7c8193888a9b12adfccb20db4f69452a72fe5bc8ddb96f1ab6b87d99537b7b kind tool-call:read]
step 15: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:6d3088ef6f53c6cef9e91dc083573c7ca2d5e24cc6a38ed7df54e39dc8a75a06 kind tool-call:read]
step 16: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:e50fa51fe297cb1d89a4694a837237f3e42b4f82caf8be66586d36c6c3add08b kind tool-call:read]
step 17: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:a3c0ef6caa00dc01a775cf859b36a55b7c2d8d99e83fea9f94896b14a1266c70 kind tool-call:read]
step 18: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:7c42603c77a1b9fb7eb9f2ba9fbeec75f8f27d8496d2b31ad1dbb85160d56d71 kind tool-call:read]
step 19: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:4cb741bfa70b349b4b0cfa8f30a572e6b48a5433d1468b8a07a0b2f5a10f066e kind tool-call:read]
step 20: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:267584b34e3681598792ab5ad5f8a4851aa23399b5bbdf7d5eba074a7f6e996e kind tool-call:read]
step 21: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:43a2849602b44e58036cf72a9cf2aade709f03a21478248b4d9042b9b926b2fb kind tool-call:read]
step 22: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:52b2c3ec9c2a22ee5e352f86bc24f7ec9b57ccc75ad0795f6f7a133d7971b4f4 kind tool-call:read]
step 23: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:070446b37e79817ce0c9eb5a54b5184de17cb9550473adce3b10e9b90e01e6ec kind tool-call:read]
step 24: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:7d5d5eac3788c9316e1a09808e181f62dca5143514b820bfebd18e715b28b166 kind tool-call:read]
step 25: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:459175a9a8137019801e9b99d443bee7e13b0f3d2df5d50c29dfbc8215de2f55 kind tool-call:read]
step 26: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:acb851c8b8d51671d471eda10c2720b62b9fc851ad73e2b5c694419c440720e6 kind tool-call:read]
step 27: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:aa7539a8c1feddc492872dcb6b5c98d2535ea3fc836d4fa8d05a3fdc3315b2bc kind tool-call:read]
step 28: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:82f72613ce04d07c7ca2d44c73b96e8ca2040a9e8cbd21013d809ab9dd524b19 kind tool-call:read]
step 29: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:721edfddf509916240866cb3f6720aa478f2ea4b3adc2fc55df11593521ca1e9 kind tool-call:read]
step 30: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:79cbffc59e58547bd37ce0fff1eedbbbf29c688c74010417f5456056d9080548 kind tool-call:read]
step 31: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:913e1807a9ac1604a3dfafe1a7be2a84457355ff2a6b540646a65ee072722f76 kind tool-call:read]
step 32: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:c6bebec546f01bbdb480b8ac9ea0ea5c0da6cad07f672b3b5f7664db0f1b0bc1 kind tool-call:read]
step 33: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:e70a8c5951b156ec5381bb85d8cf7e4d43e5422e2559ecd714875c174849d9b7 kind tool-call:read]
step 34: calling local:qwen3.8:27b
tool read <- {"path":"src/generators/utils/parse.ts"}
tool read ok: import {parseArray} from '../../utils/text';
import {indexURLTemplateList, getURLMatchesFromIndexedList} from '../../utils/url';
import type {URLTrie} from '../../utils/url';

interface SiteProps {
    url: string[];
}

export interface SiteListIndex {
    urls: readonly string[];
    domains: Readonly<{[domain: string]: number[]}>;
    domainLabels: Readonly<{[domainLabel: string]: readonly number[]}>;
    nonstandard: readonly number[];
}

export interface SitesFixesParserOptions<T> {
    commands: readonly string[];
    getCommandPropName: (command: string) => keyof T;
    parseCommandValue: (command: string, value: string) => any;
}

export type SiteFixesIndex = URLTrie<[number, number]>;

export function parseSitesFixesConfig<T extends SiteProps>(text: string, options: SitesFixesParserOptions<T>): T[] {
    const sites: T[] = [];

    const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm);
    blocks.forEach((block) => {
        const lines = block.split('\n');
        const commandIndices: number[] = [];
        lines.forEach((ln, i) => {
            if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
                commandIndices.push(i);
            }
        });

        if (commandIndices.length === 0) {
            return;
        }

        const siteFix = {
            url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as readonly string[],
        } as T;

        commandIndices.forEach((commandIndex, i) => {
            const command = lines[commandIndex].trim();
            const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n');
            const prop = options.getCommandPropName(command);
            if (!prop) {
                return;
            }
            const value = options.parseCommandValue(command, valueText);
            siteFix[prop] = value;
        });

        sites.push(siteFix);
    });

    return sites;
}

// URL patterns are guaranteed to not have protocol and leading '/'
export function getDomain(url: string): string {
    try {
        return (new URL(url)).hostname.toLowerCase();
    } catch (error) {
        return url.split('/')[0].toLowerCase();
    }
}

function processSiteFixesConfigBlock(text: string, offsets: Array<[number, number]>, recordStart: number, recordEnd: number, urls: Array<readonly string[]>) {
    // TODO: more formal definition of URLs and delimiters
    const block = text.substring(recordStart, recordEnd);
    const lines = block.split('\n');
    const commandIndices: number[] = [];
    lines.forEach((ln, i) => {
        if (ln.match(/^[A-Z]+(\s[A-Z]+){0,2}$/)) {
            commandIndices.push(i);
        }
    });

    if (commandIndices.length === 0) {
        return;
    }

    offsets.push([recordStart, recordEnd - recordStart]);

    const urls_ = parseArray(lines.slice(0, commandIndices[0]).join('\n'));
    urls.push(urls_);
}

function extractURLsFromSiteFixesConfig(text: string): {urls: string[][]; offsets: Array<[number, number]>} {
    const urls: string[][] = [];
    // Array of tuples, where first number is an offset of record start and second number is record length.
    const offsets: Array<[number, number]> = [];

    let recordStart = 0;
    // Delimiter between two blocks
    const delimiterRegex = /^\s*={2,}\s*$/gm;
    let delimiter: RegExpMatchArray | null;
    while ((delimiter = delimiterRegex.exec(text))) {
        const nextDelimiterStart = delimiter.index!;
        const nextDelimiterEnd = delimiter.index! + delimiter[0].length;
        processSiteFixesConfigBlock(text, offsets, recordStart, nextDelimiterStart, urls);
        recordStart = nextDelimiterEnd;
    }
    processSiteFixesConfigBlock(text, offsets, recordStart, text.length, urls);

    return {urls, offsets};
}

export function indexSitesFixesConfig(text: string): SiteFixesIndex {
    const {urls, offsets: offsetsGrouped} = extractURLsFromSiteFixesConfig(text);
    const offsetMap = new Map<string, [number, number]>();
    const templates: string[] = [];
    const offsets: Array<[number, number]> = [];
    urls.forEach((block, i) => {
        block.forEach((u) => {
            templates.push(u);
            offsets.push(offsetsGrouped[i]);
            offsetMap.set(u, offsetsGrouped[i]);
        });
    });
    const indexedList = indexURLTemplateList(templates, (_, i) => {
        return offsets[i];
    });
    return indexedList;
}

const siteFixesCache = new WeakMap<[number, number], any>();

export function getSitesFixesFor<T extends SiteProps>(url: string, text: string, index: SiteFixesIndex, parse: (text: string) => T[]): Array<Readonly<T>> {
    const matches = getURLMatchesFromIndexedList(url, index);

    const fixes = matches.map((offset) => {
        const cache = siteFixesCache.get(offset);
        if (cache) {
            return cache;
        }
        const [start, length] = offset;
        const block = text.slice(start, start + length);
        const fix = parse(block)[0];
        siteFixesCache.set(offset, fix);
        return fix;
    });

    if (fixes.length > 0) {
        const commonFixIndex = fixes.findIndex((f) => f.url?.[0] === '*');
        if (commonFixIndex > 0) {
            const commonFix = fixes[commonFixIndex];
            fixes.splice(commonFixIndex, 1);
            fixes.unshift(commonFix);
        }
    }

    return fixes;
}

[evidence record sha256:98323dbe3d729c5963e9f59822910ec732d38ff0538f4002aad46cd8fb2baff5 kind tool-call:read]
stopped: max-tokens after 34 steps, 1043257 tokens
gate typecheck not-applicable: package.json declares no typecheck script [evidence record sha256:81e5bc34c76ccd066aba574f815550ddb8a53ef241292bcee176472f2a07b09a]
gate lint passed: the command exited 0 [evidence record sha256:c876b9fdd87053ac5452f727ca7f51d0fe7daff553b99e48aec446fea6519a86]
gate format not-applicable: package.json declares no check-only format script, and running a writing formatter as a gate would edit the tree it is judging [evidence record sha256:6920e5a176ea5e96d174e1f9daeb0b28dc3ee2ec8a64abe88be1f4c54d7534a6]
gate tests failed: the command exited 1 [evidence record sha256:163cd8a2708eab3a2d477d7274e9b167aee80df0099f1d9f14f9270f74000b35]
gate file-set passed: nothing changed and no file set was declared, so there is nothing to check [evidence record sha256:8f060c760cb21f6bc53409b9b12c1b89574ec56b9b8479c3289fe47912fe6747]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:80a69ddf538b2edff3530ed1afc787650dc7d826a04eded035e165077cbe6257]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:48401cbfcc12987dfae6c002c84fc54a4e94fc5d6e443f68ed699bb2fc400bbf]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:d10ec5b4c8a1d40b28d094707e71408003a41b576e152d86d5eeb98612fa9caa]
gate diff-budget passed (advisory): within budget: 0 file(s) and 0 added line(s) [evidence record sha256:88aa9bd5ad9d2a92e5841f09ea57898fd1525c205fa2ffd68ed0e20022bdd6f8]
ratchet accepted attempt 2: the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage) [evidence record sha256:43eaf1c774490fcb4cda502f7b222a53ae68a2889775a5e14e6af1e208eba163]
escalated after 2 attempt(s) at gate tests: the command exited 1

no files were changed. The gates below measured an unchanged workspace, so they say nothing about work being done.

gates:
  n/a      typecheck: package.json declares no typecheck script
  passed   lint: the command exited 0
  n/a      format: package.json declares no check-only format script, and running a writing formatter as a gate would edit the tree it is judging
  failed   tests: the command exited 1
  passed   file-set: nothing changed and no file set was declared, so there is nothing to check
  passed   placeholder: no placeholder marker was introduced by this change
  passed   secret-scan: no known credential pattern appears in the added lines
  passed   behaviour-probe: 0 changed function(s) still answer to their inputs.
  passed   diff-budget (advisory): within budget: 0 file(s) and 0 added line(s)
attempt 1: accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
attempt 2: accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)

Escalating after 2 of 2 attempts.

Gate: tests (tests (npm run test))
Why: the command exited 1
Its last run is ledger record sha256:163cd8a2708eab3a2d477d7274e9b167aee80df0099f1d9f14f9270f74000b35.

Attempts:
  1. accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
     still failing: tests
  2. accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
     still failing: tests

routing reward: 0.000 (the run escalated, so the gates never went green)
[signing] the Secret Service keyring would not take a new key (secret-tool store failed: ), so the bundle is signed with a per-run key

evidence bundle: /out/bundle
verify it anywhere: node /out/bundle/verify.mjs /out/bundle
review it: open /out/bundle/review.html
what this run produced

  the page a person reads: /out/bundle/review.html
  the bundle a stranger verifies: /out/bundle
  its own verifier, needing nothing installed: node /out/bundle/verify.mjs /out/bundle
  the chain every record is on: /out/bundle/ledger.jsonl

  360 records. The harness verified 1 claim(s) and refused 0.
  bundle verified in this run: verify.mjs exited 0
