fix(changed-behavior): withhold equality for unknowns and contradictions (#7624)

find_changed_behavior derived behavior_status from each process
comparison's aggregate status alone. An unchanged comparison whose
first_divergence was unknown (residual unknowns prevent proving no
divergence) and a consistent contradiction comparison both fell
through to observed_unchanged, with no unresolved finding and no
changed-behavior registry unknown.

An unchanged comparison with unknown divergence now counts as unknown
and adds one comparison-level unresolved first_divergence finding
carrying its reason, as artifact comparisons already do for coverage.
A contradiction status makes the aggregate unknown, and contradiction
dimensions are classified and counted as contradictions instead of
unresolved branches. Complete unchanged comparisons stay
observed_unchanged and changed comparisons with unknown localization
stay observed_changed.

Fixes #7384
Fixes #7383

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Salman AlmutairiandClaude Opus 5.5 authored and GitHub committed 2026-11-05 16:56:58 +08:00
1 parent c1df51c7cb
commit e1094313f5
2 files changed
+116 -13

No files matched your search

+81 -8
View File
@@ -69,13 +69,21 @@ const comparison = (
);
};
type FixtureStatus =
| "unchanged"
| "changed"
| "unknown"
| "truncated"
| "contradiction";
const processResult = (
overrides: Partial<{
status: "unchanged" | "changed" | "unknown" | "truncated";
terminal: "unchanged" | "changed" | "unknown" | "truncated";
interaction: "unchanged" | "changed" | "unknown" | "truncated";
exit: "unchanged" | "changed" | "unknown" | "truncated";
process: "unchanged" | "changed" | "unknown" | "truncated";
status: FixtureStatus;
terminal: FixtureStatus;
interaction: FixtureStatus;
exit: FixtureStatus;
process: FixtureStatus;
first_divergence: JsonValue;
}> = {},
): JsonValue => ({
status: overrides.status ?? "unchanged",
@@ -85,7 +93,8 @@ const processResult = (
filesystem: "unchanged",
process: overrides.process ?? "unchanged",
first_divergence:
overrides.status === "changed"
overrides.first_divergence ??
(overrides.status === "changed"
? {
status: "found",
dimension: "terminal",
@@ -95,9 +104,11 @@ const processResult = (
left: "left",
right: "right",
}
: overrides.status === "truncated" || overrides.status === "unknown"
: overrides.status === "truncated" ||
overrides.status === "unknown" ||
overrides.status === "contradiction"
? { status: "unknown", reason: "Incomplete fixture." }
: { status: "none" },
: { status: "none" }),
limitations: [],
});
@@ -230,6 +241,68 @@ describe("changed behavior", () => {
});
});
describe("changed behavior withheld equality", () => {
it("withholds equality when residual unknowns prevent proving no divergence", () => {
const reason =
"Residual unknowns prevent proving that no divergence occurred.";
const evidence = comparison(
"compare_process_captures",
processResult({ first_divergence: { status: "unknown", reason } }),
);
expect(aggregate([evidence]).result).toMatchObject({
behavior_status: "unknown",
summary: { observed_changes: 0, contradictions: 0, unresolved: 1 },
findings: {
items: [
{
scope: "runtime",
dimension: "first_divergence",
classification: "unresolved_branch",
status: "unknown",
source_comparison_id: evidence.evidence_id,
limitations: [reason],
},
],
},
});
});
it("keeps a changed comparison observed when only its localization is unknown", () => {
const evidence = comparison(
"compare_process_captures",
processResult({
status: "changed",
terminal: "changed",
first_divergence: { status: "unknown", reason: "Unlocalized." },
}),
);
expect(aggregate([evidence]).result).toMatchObject({
behavior_status: "observed_changed",
summary: { observed_changes: 1, unresolved: 0 },
});
});
it("reports a process contradiction as a contradiction, never equality", () => {
const evidence = comparison(
"compare_process_captures",
processResult({ status: "contradiction", terminal: "contradiction" }),
);
expect(aggregate([evidence]).result).toMatchObject({
behavior_status: "unknown",
summary: { observed_changes: 0, contradictions: 1, unresolved: 0 },
findings: {
items: [
{
dimension: "terminal",
classification: "contradiction",
status: "contradiction",
},
],
},
});
});
});
describe("changed-behavior input and Evidence validation", () => {
it("rejects an empty comparison selection", () => {
expect(() => findChangedBehavior({ comparisons: [] })).toThrowError(
+35 -5
View File
@@ -115,7 +115,8 @@ export const changedBehaviorResultSchema = z
export type ChangedBehaviorResult = z.infer<typeof changedBehaviorResultSchema>;
type Finding = z.infer<typeof findingSchema>;
type RuntimeStatus = z.infer<typeof processCaptureComparisonSchema>["status"];
type ProcessComparison = z.infer<typeof processCaptureComparisonSchema>;
type RuntimeStatus = ProcessComparison["status"];
/** Find observed runtime changes and separately report static change candidates. */
interface FoundChangedBehavior {
@@ -146,7 +147,11 @@ export const findChangedBehavior = (input: unknown): FoundChangedBehavior => {
const findings = evidence.flatMap(findingsFor).sort(compareFindings);
const runtimeStatuses = evidence.flatMap((comparison) =>
comparison.operation === "compare_process_captures"
? [comparison.result.status]
? [
unprovenEqualityReason(comparison.result) === undefined
? comparison.result.status
: "unknown",
]
: [],
);
const links = uniqueSorted(
@@ -243,7 +248,7 @@ const processFindings = (
["filesystem", result.filesystem, "resource"],
["process", result.process, "runtime"],
] as const;
return dimensions.flatMap(([dimension, status, scope]) =>
const findings = dimensions.flatMap(([dimension, status, scope]) =>
status === "unchanged"
? []
: [
@@ -254,13 +259,37 @@ const processFindings = (
status,
classification: isChange(status)
? "observed_change"
: "unresolved_branch",
: status === "contradiction"
? "contradiction"
: "unresolved_branch",
limitations: result.limitations,
}),
],
);
const reason = unprovenEqualityReason(result);
return reason === undefined
? findings
: [
...findings,
makeFinding({
evidence,
dimension: "first_divergence",
scope: "runtime",
status: "unknown",
classification: "unresolved_branch",
limitations: [...result.limitations, reason],
}),
];
};
/** Unchanged dimensions do not establish equality while divergence stays unknown. */
const unprovenEqualityReason = (
result: ProcessComparison,
): string | undefined =>
result.status === "unchanged" && result.first_divergence.status === "unknown"
? result.first_divergence.reason
: undefined;
const functionFindings = (
comparison: Extract<
ParsedComparisonEvidence,
@@ -356,7 +385,8 @@ const behaviorStatus = (
): ChangedBehaviorResult["behavior_status"] => {
if (statuses.length === 0) return "unknown";
if (statuses.includes("truncated")) return "truncated";
if (statuses.includes("unknown")) return "unknown";
if (statuses.includes("unknown") || statuses.includes("contradiction"))
return "unknown";
return statuses.some(isChange) ? "observed_changed" : "observed_unchanged";
};