Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
111 lines
3.2 KiB
JavaScript
111 lines
3.2 KiB
JavaScript
// Split chunks line-wise for generators passed to the `std*` options
|
|
export const getSplitLinesGenerator = (binary, preserveNewlines, skipped, state) => binary || skipped
|
|
? undefined
|
|
: initializeSplitLines(preserveNewlines, state);
|
|
|
|
// Same but for synchronous methods
|
|
export const splitLinesSync = (chunk, preserveNewlines, objectMode) => objectMode
|
|
? chunk.flatMap(item => splitLinesItemSync(item, preserveNewlines))
|
|
: splitLinesItemSync(chunk, preserveNewlines);
|
|
|
|
const splitLinesItemSync = (chunk, preserveNewlines) => {
|
|
const {transform, final} = initializeSplitLines(preserveNewlines, {});
|
|
return [...transform(chunk), ...final()];
|
|
};
|
|
|
|
const initializeSplitLines = (preserveNewlines, state) => {
|
|
state.previousChunks = '';
|
|
return {
|
|
transform: splitGenerator.bind(undefined, state, preserveNewlines),
|
|
final: linesFinal.bind(undefined, state),
|
|
};
|
|
};
|
|
|
|
// This imperative logic is much faster than using `String.split()` and uses very low memory.
|
|
const splitGenerator = function * (state, preserveNewlines, chunk) {
|
|
if (typeof chunk !== 'string') {
|
|
yield chunk;
|
|
return;
|
|
}
|
|
|
|
let {previousChunks} = state;
|
|
let start = -1;
|
|
|
|
for (let end = 0; end < chunk.length; end += 1) {
|
|
if (chunk[end] === '\n') {
|
|
const newlineLength = getNewlineLength(chunk, end, preserveNewlines, state);
|
|
let line = chunk.slice(start + 1, end + 1 - newlineLength);
|
|
|
|
if (previousChunks.length > 0) {
|
|
line = concatString(previousChunks, line);
|
|
previousChunks = '';
|
|
}
|
|
|
|
yield line;
|
|
start = end;
|
|
}
|
|
}
|
|
|
|
if (start !== chunk.length - 1) {
|
|
previousChunks = concatString(previousChunks, chunk.slice(start + 1));
|
|
}
|
|
|
|
state.previousChunks = previousChunks;
|
|
};
|
|
|
|
const getNewlineLength = (chunk, end, preserveNewlines, state) => {
|
|
if (preserveNewlines) {
|
|
return 0;
|
|
}
|
|
|
|
state.isWindowsNewline = end !== 0 && chunk[end - 1] === '\r';
|
|
return state.isWindowsNewline ? 2 : 1;
|
|
};
|
|
|
|
const linesFinal = function * ({previousChunks}) {
|
|
if (previousChunks.length > 0) {
|
|
yield previousChunks;
|
|
}
|
|
};
|
|
|
|
// Unless `preserveNewlines: true` is used, we strip the newline of each line.
|
|
// This re-adds them after the user `transform` code has run.
|
|
export const getAppendNewlineGenerator = ({binary, preserveNewlines, readableObjectMode, state}) => binary || preserveNewlines || readableObjectMode
|
|
? undefined
|
|
: {transform: appendNewlineGenerator.bind(undefined, state)};
|
|
|
|
const appendNewlineGenerator = function * ({isWindowsNewline = false}, chunk) {
|
|
const {unixNewline, windowsNewline, LF, concatBytes} = typeof chunk === 'string' ? linesStringInfo : linesUint8ArrayInfo;
|
|
|
|
if (chunk.at(-1) === LF) {
|
|
yield chunk;
|
|
return;
|
|
}
|
|
|
|
const newline = isWindowsNewline ? windowsNewline : unixNewline;
|
|
yield concatBytes(chunk, newline);
|
|
};
|
|
|
|
const concatString = (firstChunk, secondChunk) => `${firstChunk}${secondChunk}`;
|
|
|
|
const linesStringInfo = {
|
|
windowsNewline: '\r\n',
|
|
unixNewline: '\n',
|
|
LF: '\n',
|
|
concatBytes: concatString,
|
|
};
|
|
|
|
const concatUint8Array = (firstChunk, secondChunk) => {
|
|
const chunk = new Uint8Array(firstChunk.length + secondChunk.length);
|
|
chunk.set(firstChunk, 0);
|
|
chunk.set(secondChunk, firstChunk.length);
|
|
return chunk;
|
|
};
|
|
|
|
const linesUint8ArrayInfo = {
|
|
windowsNewline: new Uint8Array([0x0D, 0x0A]),
|
|
unixNewline: new Uint8Array([0x0A]),
|
|
LF: 0x0A,
|
|
concatBytes: concatUint8Array,
|
|
};
|