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>
222 lines
4.5 KiB
JavaScript
222 lines
4.5 KiB
JavaScript
import {errorConstructors} from './error-constructors.js';
|
|
|
|
export class NonError extends Error {
|
|
name = 'NonError';
|
|
|
|
constructor(message) {
|
|
super(NonError._prepareSuperMessage(message));
|
|
}
|
|
|
|
static _prepareSuperMessage(message) {
|
|
try {
|
|
return JSON.stringify(message);
|
|
} catch {
|
|
return String(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
const errorProperties = [
|
|
{
|
|
property: 'name',
|
|
enumerable: false,
|
|
},
|
|
{
|
|
property: 'message',
|
|
enumerable: false,
|
|
},
|
|
{
|
|
property: 'stack',
|
|
enumerable: false,
|
|
},
|
|
{
|
|
property: 'code',
|
|
enumerable: true,
|
|
},
|
|
{
|
|
property: 'cause',
|
|
enumerable: false,
|
|
},
|
|
{
|
|
property: 'errors',
|
|
enumerable: false,
|
|
},
|
|
];
|
|
|
|
const toJsonWasCalled = new WeakSet();
|
|
|
|
const toJSON = from => {
|
|
toJsonWasCalled.add(from);
|
|
const json = from.toJSON();
|
|
toJsonWasCalled.delete(from);
|
|
return json;
|
|
};
|
|
|
|
const newError = name => {
|
|
const ErrorConstructor = errorConstructors.get(name) ?? Error;
|
|
return ErrorConstructor === AggregateError
|
|
? new ErrorConstructor([])
|
|
: new ErrorConstructor();
|
|
};
|
|
|
|
// eslint-disable-next-line complexity
|
|
const destroyCircular = ({
|
|
from,
|
|
seen,
|
|
to,
|
|
forceEnumerable,
|
|
maxDepth,
|
|
depth,
|
|
useToJSON,
|
|
serialize,
|
|
}) => {
|
|
if (!to) {
|
|
if (Array.isArray(from)) {
|
|
to = [];
|
|
} else if (!serialize && isErrorLike(from)) {
|
|
to = newError(from.name);
|
|
} else {
|
|
to = {};
|
|
}
|
|
}
|
|
|
|
seen.push(from);
|
|
|
|
if (depth >= maxDepth) {
|
|
return to;
|
|
}
|
|
|
|
if (useToJSON && typeof from.toJSON === 'function' && !toJsonWasCalled.has(from)) {
|
|
return toJSON(from);
|
|
}
|
|
|
|
const continueDestroyCircular = value => destroyCircular({
|
|
from: value,
|
|
seen: [...seen],
|
|
forceEnumerable,
|
|
maxDepth,
|
|
depth,
|
|
useToJSON,
|
|
serialize,
|
|
});
|
|
|
|
for (const [key, value] of Object.entries(from)) {
|
|
if (value && value instanceof Uint8Array && value.constructor.name === 'Buffer') {
|
|
to[key] = '[object Buffer]';
|
|
continue;
|
|
}
|
|
|
|
// TODO: Use `stream.isReadable()` when targeting Node.js 18.
|
|
if (value !== null && typeof value === 'object' && typeof value.pipe === 'function') {
|
|
to[key] = '[object Stream]';
|
|
continue;
|
|
}
|
|
|
|
if (typeof value === 'function') {
|
|
continue;
|
|
}
|
|
|
|
if (!value || typeof value !== 'object') {
|
|
// Gracefully handle non-configurable errors like `DOMException`.
|
|
try {
|
|
to[key] = value;
|
|
} catch {}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!seen.includes(from[key])) {
|
|
depth++;
|
|
to[key] = continueDestroyCircular(from[key]);
|
|
|
|
continue;
|
|
}
|
|
|
|
to[key] = '[Circular]';
|
|
}
|
|
|
|
if (serialize || to instanceof Error) {
|
|
for (const {property, enumerable} of errorProperties) {
|
|
if (from[property] !== undefined && from[property] !== null) {
|
|
Object.defineProperty(to, property, {
|
|
value: isErrorLike(from[property]) || Array.isArray(from[property])
|
|
? continueDestroyCircular(from[property])
|
|
: from[property],
|
|
enumerable: forceEnumerable ? true : enumerable,
|
|
configurable: true,
|
|
writable: true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return to;
|
|
};
|
|
|
|
export function serializeError(value, options = {}) {
|
|
const {
|
|
maxDepth = Number.POSITIVE_INFINITY,
|
|
useToJSON = true,
|
|
} = options;
|
|
|
|
if (typeof value === 'object' && value !== null) {
|
|
return destroyCircular({
|
|
from: value,
|
|
seen: [],
|
|
forceEnumerable: true,
|
|
maxDepth,
|
|
depth: 0,
|
|
useToJSON,
|
|
serialize: true,
|
|
});
|
|
}
|
|
|
|
// People sometimes throw things besides Error objects…
|
|
if (typeof value === 'function') {
|
|
// `JSON.stringify()` discards functions. We do too, unless a function is thrown directly.
|
|
// We intentionally use `||` because `.name` is an empty string for anonymous functions.
|
|
return `[Function: ${value.name || 'anonymous'}]`;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
export function deserializeError(value, options = {}) {
|
|
const {maxDepth = Number.POSITIVE_INFINITY} = options;
|
|
|
|
if (value instanceof Error) {
|
|
return value;
|
|
}
|
|
|
|
if (isMinimumViableSerializedError(value)) {
|
|
return destroyCircular({
|
|
from: value,
|
|
seen: [],
|
|
to: newError(value.name),
|
|
maxDepth,
|
|
depth: 0,
|
|
serialize: false,
|
|
});
|
|
}
|
|
|
|
return new NonError(value);
|
|
}
|
|
|
|
export function isErrorLike(value) {
|
|
return Boolean(value)
|
|
&& typeof value === 'object'
|
|
&& typeof value.name === 'string'
|
|
&& typeof value.message === 'string'
|
|
&& typeof value.stack === 'string';
|
|
}
|
|
|
|
// Used as a weak check for immediately-passed objects, whereas `isErrorLike` is used for nested values to avoid bad detection
|
|
function isMinimumViableSerializedError(value) {
|
|
return Boolean(value)
|
|
&& typeof value === 'object'
|
|
&& typeof value.message === 'string'
|
|
&& !Array.isArray(value);
|
|
}
|
|
|
|
export {addKnownErrorConstructor} from './error-constructors.js';
|