mirror of
https://github.com/SHOGGOTH-SECTOR/sica-fondt.git
synced 2026-08-01 08:30:20 +00:00
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):
- brain/ LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
dispatch, A51 channels, and the OSINT cluster
- knowledge/ LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
MITRE ATT&CK data
- reference/ defensive threat-reference (C3, shhbruh doc) + AdaYaml parser
License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.
Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.
https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
237 lines
9.9 KiB
TypeScript
237 lines
9.9 KiB
TypeScript
/**
|
|
* Copyright 2026 Reto Meier
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { FuzzyReplaceResult } from "../momoa_core/types.js";
|
|
|
|
interface Match {
|
|
startIndex: number;
|
|
endIndex: number;
|
|
matchedText: string;
|
|
}
|
|
|
|
function escapeRegExp(str: string): string {
|
|
// $& means the whole matched string
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function getLineStartIndices(text: string): number[] {
|
|
const indices: number[] = [0];
|
|
const newlineRegex = /\r?\n/g;
|
|
let match: RegExpExecArray | null;
|
|
|
|
while ((match = newlineRegex.exec(text)) !== null) {
|
|
indices.push(match.index + match[0].length);
|
|
}
|
|
return indices;
|
|
}
|
|
|
|
function findLineNumber(charIndex: number, lineStartIndices: number[]): number {
|
|
// Search from the end for efficiency
|
|
for (let i = lineStartIndices.length - 1; i >= 0; i--) {
|
|
if (charIndex >= lineStartIndices[i]) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
function generateNoMatchError(originalString: string, textToReplace: string): string {
|
|
// Attempt to find a match for an initial segment of the text to provide a better error.
|
|
const ttrLines = textToReplace.split(/\r?\n/);
|
|
let ttrSearchPrefixContent = ttrLines[0];
|
|
if (ttrLines.length > 1 && ttrSearchPrefixContent.length < 30) {
|
|
ttrSearchPrefixContent += `\n${ttrLines[1]}`;
|
|
}
|
|
ttrSearchPrefixContent = ttrSearchPrefixContent.substring(0, 120);
|
|
|
|
const searchPrefixParts = ttrSearchPrefixContent.split(/\s+/).filter(p => p.length > 0);
|
|
|
|
if (searchPrefixParts.length > 0) {
|
|
try {
|
|
const prefixPatternRegex = new RegExp(searchPrefixParts.map(escapeRegExp).join('\\s+'), 'i');
|
|
const prefixMatchResult = prefixPatternRegex.exec(originalString);
|
|
|
|
if (prefixMatchResult) {
|
|
const matchedPrefixInOS = prefixMatchResult[0];
|
|
let displaySearchedPrefix = searchPrefixParts.join(' ');
|
|
if (displaySearchedPrefix.length > 40) displaySearchedPrefix = `${displaySearchedPrefix.substring(0, 37)}...`;
|
|
|
|
let displayMatchedPrefixInOS = matchedPrefixInOS.replace(/\s+/g, ' ');
|
|
if (displayMatchedPrefixInOS.length > 60) displayMatchedPrefixInOS = `${displayMatchedPrefixInOS.substring(0, 57)}...`;
|
|
|
|
return `The text to replace was not found. A segment starting with content similar to '${displaySearchedPrefix}' (found in file as '${displayMatchedPrefixInOS}') was identified, but the full block did not match. Please check for discrepancies after this initial segment.`;
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
// Generic fallback error.
|
|
let ttrExcerptForDisplay = textToReplace.replace(/\s+/g, ' ').trim().substring(0, 70);
|
|
if (textToReplace.length > 70) ttrExcerptForDisplay += "...";
|
|
return `The text to replace (starting with '${ttrExcerptForDisplay}') was not found. Key components seem to be missing or significantly different in the file.`;
|
|
}
|
|
|
|
function performSingleFuzzyReplace(originalString: string, match: Match, textToReplace: string, replacementText: string): string {
|
|
const coreMatchStartInOS = match.startIndex;
|
|
const coreContentMatchedInOS = match.matchedText;
|
|
const coreContentEndInOS = coreMatchStartInOS + coreContentMatchedInOS.length;
|
|
|
|
// --- Inlined Helper Logic for Trailing Whitespace Analysis ---
|
|
const trailingWhitespaceMatch = textToReplace.match(/(\s*)$/);
|
|
const ttrTrailingWhitespaceText = trailingWhitespaceMatch ? (trailingWhitespaceMatch[1] || "") : "";
|
|
const newlineMatches = ttrTrailingWhitespaceText.match(/\r?\n/g);
|
|
const numExpectedNewlinesInTTR = newlineMatches ? newlineMatches.length : 0;
|
|
const ttrTrailIsPurelyNewlines = /^[\r\n]*$/.test(ttrTrailingWhitespaceText);
|
|
// --- End Inlined Logic ---
|
|
|
|
// The regex only matches content, not surrounding whitespace.
|
|
// We must now match the TRAILING whitespace of `textToReplace` in the `originalString`
|
|
// to determine the precise end of the segment to be replaced.
|
|
let suffixStartIndex = coreContentEndInOS;
|
|
let newlinesConsumedFromOS = 0;
|
|
let tempOsIndex = coreContentEndInOS;
|
|
|
|
while (tempOsIndex < originalString.length) {
|
|
if (newlinesConsumedFromOS >= numExpectedNewlinesInTTR && ttrTrailIsPurelyNewlines) break;
|
|
|
|
const char = originalString[tempOsIndex];
|
|
if (char === '\n' || (char === '\r' && originalString[tempOsIndex + 1] === '\n')) {
|
|
tempOsIndex += (char === '\r' ? 2 : 1);
|
|
newlinesConsumedFromOS++;
|
|
} else if (char === ' ' || char === '\t') {
|
|
if (ttrTrailIsPurelyNewlines) break;
|
|
tempOsIndex++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
suffixStartIndex = tempOsIndex;
|
|
|
|
const prefix = originalString.substring(0, coreMatchStartInOS);
|
|
const suffix = originalString.substring(suffixStartIndex);
|
|
const modifiedString = prefix + replacementText + suffix;
|
|
|
|
return modifiedString;
|
|
}
|
|
|
|
function generateDisambiguationSnippets(originalString: string, matches: Match[]): string[] {
|
|
const disambiguationSnippets: string[] = [];
|
|
const originalLinesArray = originalString.split(/\r?\n/);
|
|
const lineStartIndices = getLineStartIndices(originalString);
|
|
|
|
for (const match of matches) {
|
|
const matchStartLineNum = findLineNumber(match.startIndex, lineStartIndices);
|
|
const matchEndLineNum = findLineNumber(Math.max(0, match.endIndex - 1), lineStartIndices);
|
|
|
|
// Find the first non-empty preceding line for context
|
|
let contextStartLineNum = matchStartLineNum;
|
|
for (let i = matchStartLineNum - 2; i >= 0; i--) {
|
|
if (originalLinesArray[i].trim() !== '') {
|
|
contextStartLineNum = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
contextStartLineNum = Math.max(1, contextStartLineNum);
|
|
|
|
// Find the first non-empty succeeding line for context
|
|
let contextEndLineNum = matchEndLineNum;
|
|
for (let i = matchEndLineNum; i < originalLinesArray.length; i++) {
|
|
if (originalLinesArray[i].trim() !== '') {
|
|
contextEndLineNum = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
contextEndLineNum = Math.min(originalLinesArray.length, contextEndLineNum);
|
|
|
|
const snippetStartCharIndex = lineStartIndices[contextStartLineNum - 1];
|
|
const snippetEndCharIndex = (contextEndLineNum < lineStartIndices.length)
|
|
? lineStartIndices[contextEndLineNum]
|
|
: originalString.length;
|
|
|
|
let snippet = originalString.slice(snippetStartCharIndex, snippetEndCharIndex);
|
|
|
|
// Trim trailing newline that comes from slicing up to the start of the next line
|
|
if (snippet.endsWith('\r\n')) {
|
|
snippet = snippet.slice(0, -2);
|
|
} else if (snippet.endsWith('\n')) {
|
|
snippet = snippet.slice(0, -1);
|
|
}
|
|
disambiguationSnippets.push(snippet);
|
|
}
|
|
return disambiguationSnippets;
|
|
}
|
|
|
|
/**
|
|
* Replaces a substring within an original string, ignoring whitespace differences
|
|
* (including line breaks) and case differences. It preserves original indentation on replacement.
|
|
*
|
|
* @param originalString The string to perform the replacement in.
|
|
* @param textToReplace The substring to find (potentially with different whitespace/case).
|
|
* @param replacementText The text to insert.
|
|
*/
|
|
export function fuzzyReplace(
|
|
originalString: string,
|
|
textToReplace: string,
|
|
replacementText: string
|
|
): FuzzyReplaceResult {
|
|
const parts = textToReplace.split(/\s+/).filter(part => part.length > 0);
|
|
|
|
if (parts.length === 0) {
|
|
return {error: `The specified text to replace is empty or contains only whitespace. No replacement possible.`};
|
|
}
|
|
|
|
// Fast path: If there's exactly one literal (but case-insensitive) match, perform a simple replacement.
|
|
if (textToReplace) {
|
|
const escapedSubStringForCount = escapeRegExp(textToReplace);
|
|
// A positive lookahead `(?=...)` allows for overlapping matches because it doesn't consume characters.
|
|
const countRegex = new RegExp(`(?=(${escapedSubStringForCount}))`, 'gi');
|
|
const occurrences = Array.from(originalString.matchAll(countRegex)).length;
|
|
|
|
if (occurrences === 1) {
|
|
const replaceRegex = new RegExp(escapedSubStringForCount, 'i');
|
|
const simpleReplaceString = originalString.replace(replaceRegex, replacementText);
|
|
return {modifiedString: simpleReplaceString };
|
|
}
|
|
}
|
|
|
|
// Fuzzy match path:
|
|
const pattern = parts.map(escapeRegExp).join('\\s+');
|
|
const searchRegex = new RegExp(pattern, 'gi');
|
|
const matches: Match[] = [];
|
|
let matchResult: RegExpExecArray | null;
|
|
|
|
while ((matchResult = searchRegex.exec(originalString)) !== null) {
|
|
matches.push({
|
|
startIndex: matchResult.index,
|
|
endIndex: matchResult.index + matchResult[0].length,
|
|
matchedText: matchResult[0]
|
|
});
|
|
// Prevent infinite loops on zero-length matches
|
|
if (matchResult[0].length === 0) {
|
|
searchRegex.lastIndex++;
|
|
}
|
|
}
|
|
|
|
// Dispatch to the appropriate handler based on the number of matches.
|
|
if (matches.length === 0) {
|
|
return { error: generateNoMatchError(originalString, textToReplace) };
|
|
} else if (matches.length === 1) {
|
|
return { modifiedString: performSingleFuzzyReplace(originalString, matches[0], textToReplace, replacementText) };
|
|
} else {
|
|
return { multipleMatches : generateDisambiguationSnippets(originalString, matches) };
|
|
}
|
|
}
|