refactor(evidence): centralize comparison identities and source recording

This commit is contained in:
morluto
2026-10-22 07:45:47 +08:00
parent 452e3ccad1
commit a59abe932e
11 changed files with 96 additions and 156 deletions
-15
View File
@@ -3,7 +3,6 @@ import {
createAnalysisProfile,
type AnalysisProfileCommitment,
} from "../domain/analysisProfile.js";
/** Provider identity for deterministic composed analysis workflows. */
export const REA_WORKFLOW_PROVIDER = {
id: "rea-workflow",
@@ -112,20 +111,6 @@ export const JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER = {
version: "1",
} as const;
/** Provider identity for deterministic cross-artifact comparisons. */
export const ARTIFACT_COMPARISON_PROVIDER = {
id: "rea-artifact-comparison",
name: "REA artifact comparison",
version: "1",
} as const;
/** Provider identity for deterministic cross-function comparisons. */
export const FUNCTION_COMPARISON_PROVIDER = {
id: "rea-function-comparison",
name: "REA function comparison",
version: "1",
} as const;
/** Provider identity for deterministic Evidence bundle comparisons. */
export const BUNDLE_COMPARISON_PROVIDER = {
id: "rea-bundle-comparison",
@@ -25,7 +25,7 @@ import {
type EvidenceLookup,
} from "../EvidenceInputResolver.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../InvestigationProviders.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
const OPERATION = "match_native_functions";
export interface NativeFunctionMatchingResult {
@@ -3,6 +3,7 @@ import {
wasmInterfaceEntryRepresentationBytes,
} from "../../domain/wasm/wasmInterface.js";
import { setImmediate } from "node:timers/promises";
import { ARTIFACT_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
import {
compareWasmInterfacesInputSchema,
wasmInterfaceComparisonSchema,
@@ -26,7 +27,6 @@ import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
import { ARTIFACT_COMPARISON_PROVIDER } from "../InvestigationProviders.js";
import {
resolvePairedEvidenceRequest,
type EvidenceLookup,
+2 -1
View File
@@ -3,6 +3,7 @@ import type { Logger } from "pino";
import { matchNativeFunctionsFromBundles } from "../application/native/NativeFunctionMatchingService.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { matchNativeFunctionsInputSchema } from "../contracts/native/functionMatchingContracts.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";
@@ -66,7 +67,7 @@ export const registerNativeFunctionMatchingCommands = (
),
);
const delivery = {
providerId: "rea-function-comparison",
providerId: FUNCTION_COMPARISON_PROVIDER.id,
operation: "match_native_functions",
signal,
detail: parsed.data.detail,
+2 -5
View File
@@ -1,5 +1,6 @@
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js";
import { compareProcessCaptures } from "../domain/process/processComparison.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "./functionComparisonExample.js";
@@ -13,11 +14,7 @@ const comparison = compareFunctions(
export const FUNCTION_COMPARISON_EVIDENCE = createEvidence(
undefined,
{
id: "rea-function-comparison",
name: "REA function comparison",
version: "1",
},
FUNCTION_COMPARISON_PROVIDER,
{
predicateType: "rea.function-comparison",
operation: "compare_functions",
+7 -48
View File
@@ -11,11 +11,11 @@ import {
comparisonSourceEvidenceIds,
} from "./comparisonSourceEvidence.js";
import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
import {
PROCESS_PROVIDER,
isProcessEvidenceProvider,
} from "./process/processEvidenceProvider.js";
hasComparisonEvidenceIdentity,
isComparisonEvidenceOperation,
} from "./comparisonEvidenceIdentity.js";
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
import {
comparisonStatusSchema,
deriveProcessComparisonStatus,
@@ -111,30 +111,6 @@ type ParsedComparison =
readonly result: z.infer<typeof artifactComparisonResultSchema>;
};
const EXPECTED_PROVIDERS = {
compare_process_captures: {
predicates: ["rea.process-comparison"],
...PROCESS_PROVIDER,
},
compare_artifacts: {
predicates: ["rea.artifact-comparison"],
id: "rea-artifact-comparison",
name: "REA artifact comparison",
version: "1",
},
compare_functions: {
predicates: ["rea.function-comparison"],
id: "rea-function-comparison",
name: "REA function comparison",
version: "1",
},
} as const;
const acceptsPredicate = (
expected: readonly string[],
actual: string,
): boolean => expected.some((predicate) => predicate === actual);
/** Find observed runtime changes and separately report static change candidates. */
interface FoundChangedBehavior {
readonly result: ChangedBehaviorResult;
@@ -202,29 +178,12 @@ export const findChangedBehavior = (input: unknown): FoundChangedBehavior => {
const parseComparisonEvidence = (input: unknown): ParsedComparison => {
const evidence = parseEvidence(input);
if (!(evidence.operation in EXPECTED_PROVIDERS))
if (!isComparisonEvidenceOperation(evidence.operation))
throw new TypeError(
"Changed-behavior analysis requires process, artifact, or function comparison Evidence",
);
const operation = z
.enum([
"compare_process_captures",
"compare_artifacts",
"compare_functions",
])
.parse(evidence.operation);
const expected = EXPECTED_PROVIDERS[operation];
if (
!acceptsPredicate(expected.predicates, evidence.predicate_type) ||
(operation === "compare_process_captures"
? !isProcessEvidenceProvider(evidence.provider)
: evidence.provider.id !== expected.id ||
evidence.provider.name !== expected.name ||
evidence.provider.version !== expected.version) ||
evidence.confidence !== "derived" ||
evidence.authority !== "analyst-inference" ||
evidence.subject !== null
)
const operation = evidence.operation;
if (!hasComparisonEvidenceIdentity(evidence))
throw new TypeError(
"Comparison Evidence operation, predicate, and provider disagree",
);
+64
View File
@@ -0,0 +1,64 @@
import type { Evidence } from "./evidence.js";
import {
PROCESS_PROVIDER,
isProcessEvidenceProvider,
} from "./process/processEvidenceProvider.js";
/** Canonical provider identity for deterministic artifact comparisons. */
export const ARTIFACT_COMPARISON_PROVIDER = {
id: "rea-artifact-comparison",
name: "REA artifact comparison",
version: "1",
} as const;
/** Canonical provider identity for deterministic function comparisons. */
export const FUNCTION_COMPARISON_PROVIDER = {
id: "rea-function-comparison",
name: "REA function comparison",
version: "1",
} as const;
const COMPARISON_EVIDENCE_IDENTITIES = {
compare_artifacts: {
predicate: "rea.artifact-comparison",
provider: ARTIFACT_COMPARISON_PROVIDER,
},
compare_functions: {
predicate: "rea.function-comparison",
provider: FUNCTION_COMPARISON_PROVIDER,
},
compare_process_captures: {
predicate: "rea.process-comparison",
provider: PROCESS_PROVIDER,
},
} as const;
type ComparisonEvidenceOperation =
keyof typeof COMPARISON_EVIDENCE_IDENTITIES;
/** Whether an operation is one of the comparison workflows this domain owns. */
export const isComparisonEvidenceOperation = (
operation: string,
): operation is ComparisonEvidenceOperation =>
Object.hasOwn(COMPARISON_EVIDENCE_IDENTITIES, operation);
/** Check shared Evidence identity without imposing a consumer's link policy. */
export const hasComparisonEvidenceIdentity = (
evidence: Evidence,
): boolean => {
if (!isComparisonEvidenceOperation(evidence.operation)) return false;
const expected = COMPARISON_EVIDENCE_IDENTITIES[evidence.operation];
const providerMatches =
evidence.operation === "compare_process_captures"
? isProcessEvidenceProvider(evidence.provider)
: evidence.provider.id === expected.provider.id &&
evidence.provider.name === expected.provider.name &&
evidence.provider.version === expected.provider.version;
return (
evidence.predicate_type === expected.predicate &&
providerMatches &&
evidence.confidence === "derived" &&
evidence.authority === "analyst-inference" &&
evidence.subject === null
);
};
+8 -38
View File
@@ -12,12 +12,9 @@ import {
assertComparisonFindingLinks,
} from "./comparisonSourceEvidence.js";
import { parseEvidence, type Evidence } from "./evidence.js";
import { hasComparisonEvidenceIdentity } from "./comparisonEvidenceIdentity.js";
import { parseEvidenceBundle } from "./evidenceBundle.js";
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
import {
PROCESS_PROVIDER,
isProcessEvidenceProvider,
} from "./process/processEvidenceProvider.js";
import { functionDossierSchema } from "./hopperValues.js";
import { assessFunctionCallCoverage } from "./functionCallCoverage.js";
import { processCaptureComparisonSchema } from "./process/processComparison.js";
@@ -38,24 +35,10 @@ import {
} from "./reconstructionUnknowns.js";
import type { ResidualUnknown } from "./residualUnknown.js";
const providers = {
behavioral: {
operation: "compare_process_captures",
predicates: ["rea.process-comparison"],
...PROCESS_PROVIDER,
},
"structural-function": {
operation: "compare_functions",
predicates: ["rea.function-comparison"],
id: "rea-function-comparison",
name: "REA function comparison",
},
"structural-artifact": {
operation: "compare_artifacts",
predicates: ["rea.artifact-comparison"],
id: "rea-artifact-comparison",
name: "REA artifact comparison",
},
const comparisonOperationsByClaimKind = {
behavioral: "compare_process_captures",
"structural-function": "compare_functions",
"structural-artifact": "compare_artifacts",
} as const;
interface FunctionSourceFacts {
@@ -64,11 +47,6 @@ interface FunctionSourceFacts {
readonly callCoverageDiagnostics: readonly string[];
}
const acceptsPredicate = (
expected: readonly string[],
actual: string,
): boolean => expected.some((predicate) => predicate === actual);
/** Verify only explicitly declared claims; no global equivalence is inferred. */
export const verifyReconstruction = (
specificationInput: unknown,
@@ -234,18 +212,10 @@ const readValidatedSourceFacts = (
const validateComparisonIdentity = (claim: Claim, evidence: Evidence): void => {
parseEvidence(evidence);
const expected = providers[claim.kind];
const expectedOperation = comparisonOperationsByClaimKind[claim.kind];
if (
evidence.operation !== expected.operation ||
!acceptsPredicate(expected.predicates, evidence.predicate_type) ||
(claim.kind === "behavioral"
? !isProcessEvidenceProvider(evidence.provider)
: evidence.provider.id !== expected.id ||
evidence.provider.name !== expected.name ||
evidence.provider.version !== "1") ||
evidence.confidence !== "derived" ||
evidence.authority !== "analyst-inference" ||
evidence.subject !== null
evidence.operation !== expectedOperation ||
!hasComparisonEvidenceIdentity(evidence)
)
throw new TypeError("Comparison Evidence identity or authority disagrees");
};
+2 -39
View File
@@ -11,11 +11,8 @@ import {
assertComparisonFindingLinks,
} from "./comparisonSourceEvidence.js";
import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
import { hasComparisonEvidenceIdentity } from "./comparisonEvidenceIdentity.js";
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
import {
PROCESS_PROVIDER,
isProcessEvidenceProvider,
} from "./process/processEvidenceProvider.js";
import type { JsonValue } from "./jsonValue.js";
import {
comparisonStatusSchema,
@@ -145,26 +142,6 @@ type Mapping = z.infer<typeof mappingSchema>;
type CorrelationItem = z.infer<typeof correlationItemSchema>;
type ComparisonStatus = z.infer<typeof comparisonStatusSchema>;
type ComparisonIdentity = readonly [string | readonly string[], string, string];
const EXPECTED: Readonly<Record<string, ComparisonIdentity>> = {
compare_artifacts: [
"rea.artifact-comparison",
"rea-artifact-comparison",
"REA artifact comparison",
],
compare_functions: [
"rea.function-comparison",
"rea-function-comparison",
"REA function comparison",
],
compare_process_captures: [
"rea.process-comparison",
PROCESS_PROVIDER.id,
PROCESS_PROVIDER.name,
],
};
/** Correlate only caller-declared static/runtime hypotheses. */
export const correlateStaticAndRuntime = (
input: unknown,
@@ -381,21 +358,7 @@ const parseRuntime = (input: unknown): Evidence => {
};
const assertComparisonIdentity = (evidence: Evidence): void => {
const expected = EXPECTED[evidence.operation];
if (
expected === undefined ||
!(Array.isArray(expected[0])
? expected[0].includes(evidence.predicate_type)
: evidence.predicate_type === expected[0]) ||
(evidence.operation === "compare_process_captures"
? !isProcessEvidenceProvider(evidence.provider)
: evidence.provider.id !== expected[1] ||
evidence.provider.name !== expected[2] ||
evidence.provider.version !== "1") ||
evidence.confidence !== "derived" ||
evidence.authority !== "analyst-inference" ||
evidence.subject !== null
)
if (!hasComparisonEvidenceIdentity(evidence))
throw new TypeError(
"Comparison Evidence operation, predicate, provider, or citations disagree",
);
+8 -7
View File
@@ -3,16 +3,17 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import { compareArtifacts } from "../domain/artifactComparison.js";
import { ARTIFACT_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js";
import { createEvidence, parseEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { resolveArtifactComparisonEvidence } from "../application/artifacts/ArtifactComparisonEvidence.js";
import type { RecordUnknownInput } from "../domain/residualUnknown.js";
import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { ARTIFACT_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
import { recordSessionEvidenceSources } from "./sessionEvidence.js";
/** Register Evidence-backed deterministic artifact comparison. */
export const registerArtifactComparisonTool = (
@@ -58,12 +59,12 @@ export const registerArtifactComparisonTool = (
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const { left, right, comparison } = computed.value;
const sources = [left, right];
for (const source of sources) {
const recordedSource = session.recordEvidence(source);
if (!recordedSource.ok)
return server.delivery.toCallToolResult(recordedSource, contract);
}
const recordedSources = recordSessionEvidenceSources(
(source) => session.recordEvidence(source),
[left, right],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
const leftEvidenceIds = [left.evidence_id];
const rightEvidenceIds = [right.evidence_id];
const evidence = createEvidence(
+1 -1
View File
@@ -8,6 +8,7 @@ import {
parseEvidence,
type Evidence,
} from "../domain/evidence.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { err } from "../domain/result.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
@@ -15,7 +16,6 @@ import type { RecordUnknownInput } from "../domain/residualUnknown.js";
import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";