mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
feat(sqlite): inspect offline snapshots with selective evidence (#2271)
Add offline SQLite database/WAL inspection through the shared CLI/MCP. Use compact inventory, selected-table schema, exact ordered columns and bounded row samples while preserving source identity and lossless values. Retain completed Evidence through late cancellation and private-root cleanup failures, bound concurrent inspections, and recheck journal validity after acquisition. Reuse prepared CI runtime artifacts across supported Node lines. Co-authored-by: Y0UW1SH <29498139+Y0UW1SH@users.noreply.github.com> Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
1 parent
916ac97bf9
commit
aeee5074d1
59 files changed
+5757
-24
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { SqliteDatabasePort } from "./SqliteDatabasePort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
createEvidence,
|
||||
type Evidence,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectSqliteDatabaseInputSchema,
|
||||
sqliteDatabaseSchema,
|
||||
type InspectSqliteDatabaseInput,
|
||||
type SqliteDatabase,
|
||||
SQLITE_ROW_LIMIT_DEFAULT,
|
||||
sqliteSchemaScope,
|
||||
sqliteInspectionParameters,
|
||||
} from "../../domain/sqlite/sqliteDatabase.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
|
||||
const OPERATION = "inspect_sqlite_database";
|
||||
|
||||
const matchesSelectedDatabase = (
|
||||
report: SqliteDatabase,
|
||||
input: InspectSqliteDatabaseInput,
|
||||
subject: EvidenceSubjectTarget,
|
||||
): boolean =>
|
||||
report.artifact.path === input.path &&
|
||||
subject.path === input.path &&
|
||||
subject.sha256 === report.artifact.sha256 &&
|
||||
subject.format === "file" &&
|
||||
(report.wal === null || report.wal.path === `${input.path}-wal`) &&
|
||||
report.schema.scope === sqliteSchemaScope(input) &&
|
||||
(report.schema.scope !== "selected_table" ||
|
||||
(report.schema.tables.length === 1 &&
|
||||
report.schema.tables[0]?.name === input.table &&
|
||||
report.schema.indexes.every((index) => index.table === input.table) &&
|
||||
report.schema.triggers.every(
|
||||
(trigger) => trigger.table === input.table,
|
||||
) &&
|
||||
report.schema.views.length === 0)) &&
|
||||
(input.table === undefined
|
||||
? report.rows === null
|
||||
: report.rows?.table === input.table &&
|
||||
report.rows.row_limit === (input.row_limit ?? SQLITE_ROW_LIMIT_DEFAULT) &&
|
||||
(input.columns === undefined ||
|
||||
(report.rows.columns.length === input.columns.length &&
|
||||
report.rows.columns.every(
|
||||
(name, index) => name === input.columns?.[index],
|
||||
))));
|
||||
|
||||
/** Shared snapshot inspection and caller-selection validation for CLI and MCP. */
|
||||
export class SqliteDatabaseService {
|
||||
constructor(readonly provider: SqliteDatabasePort) {}
|
||||
|
||||
/** Await provider shutdown, retaining failed cleanup owners for retry. */
|
||||
close(): Promise<void> {
|
||||
return this.provider.close?.() ?? Promise.resolve();
|
||||
}
|
||||
|
||||
/** Inspect an explicit database snapshot and preserve its original database/WAL identity. */
|
||||
async inspect(
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
const input = inspectSqliteDatabaseInputSchema.safeParse(rawInput);
|
||||
if (!input.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
|
||||
cause: input.error,
|
||||
}),
|
||||
);
|
||||
if (!isAbsolute(input.data.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const inspected = await this.provider.inspect(input.data, options);
|
||||
if (!inspected.ok) return inspected;
|
||||
const execution = inspected.value;
|
||||
const report = sqliteDatabaseSchema.safeParse(execution.result);
|
||||
if (
|
||||
!report.success ||
|
||||
execution.subject === null ||
|
||||
!matchesSelectedDatabase(report.data, input.data, execution.subject)
|
||||
)
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"SQLite adapter returned malformed data or changed the selected database identity/table/row limit.",
|
||||
),
|
||||
);
|
||||
return ok(
|
||||
createEvidence(execution.subject, execution.provider, {
|
||||
operation: OPERATION,
|
||||
parameters: sqliteInspectionParameters(input.data),
|
||||
result: jsonValueSchema.parse(report.data),
|
||||
...(execution.rawResult === null
|
||||
? {}
|
||||
: { rawResult: execution.rawResult }),
|
||||
confidence: "observed",
|
||||
locations: execution.locations,
|
||||
limitations: execution.limitations,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user