fix(workflows): retain residual unknowns through CLI and MCP
This commit is contained in:
parent
65a6288257
commit
0136ee8c22
|
|
@ -44,6 +44,7 @@ import {
|
|||
import {
|
||||
createWorkflowEvidence,
|
||||
workflowSnapshotRecord,
|
||||
recordWorkflowUnknowns,
|
||||
} from "./WorkflowEvidence.js";
|
||||
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
|
||||
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
|
||||
|
|
@ -262,7 +263,19 @@ const runAnalysis = async (
|
|||
signal,
|
||||
evidenceProfile,
|
||||
});
|
||||
if (evidence !== undefined) session.recordEvidence(evidence);
|
||||
if (evidence !== undefined) {
|
||||
const recorded = session.recordEvidence(evidence);
|
||||
if (!recorded.ok) return cliError(recorded.error);
|
||||
if (isWorkflowEvidenceTool(tool)) {
|
||||
const unknowns = recordWorkflowUnknowns({
|
||||
name: tool,
|
||||
result: evidence.normalized_result,
|
||||
evidenceId: evidence.evidence_id,
|
||||
recordUnknown: (unknown) => session.recordUnknown(unknown),
|
||||
});
|
||||
if (!unknowns.ok) return cliError(unknowns.error);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isWorkflowEvidenceTool(tool) &&
|
||||
tool !== "trace_native_ui_action" &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
recordWorkflowUnknowns,
|
||||
createWorkflowEvidence,
|
||||
} from "./WorkflowEvidence.js";
|
||||
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
|
||||
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
|
||||
import { err } from "../domain/result.js";
|
||||
|
||||
describe("workflow residual question retention", () => {
|
||||
it("keeps questions linked to workflow Evidence and records repeats idempotently", () => {
|
||||
const records = new InvestigationRecords();
|
||||
const result = {
|
||||
residual_unknowns: ["Which provider resolves this boundary?"],
|
||||
};
|
||||
const evidence = createWorkflowEvidence({
|
||||
target: undefined,
|
||||
operation: "inspect_native_api",
|
||||
parameters: { procedure: "0x1000" },
|
||||
result,
|
||||
upstreamProfile: undefined,
|
||||
});
|
||||
expect(records.recordEvidence(evidence).ok).toBe(true);
|
||||
const input = {
|
||||
name: "inspect_native_api",
|
||||
result,
|
||||
evidenceId: evidence.evidence_id,
|
||||
recordUnknown: (
|
||||
unknown: Parameters<InvestigationRecords["recordUnknown"]>[0],
|
||||
) => records.recordUnknown(unknown),
|
||||
};
|
||||
expect(recordWorkflowUnknowns(input).ok).toBe(true);
|
||||
expect(recordWorkflowUnknowns(input).ok).toBe(true);
|
||||
expect(records.listUnknowns()).toMatchObject([
|
||||
{
|
||||
question: result.residual_unknowns[0],
|
||||
domain: "native-api",
|
||||
supporting_evidence_ids: [evidence.evidence_id],
|
||||
required_authority: "shipped-artifact",
|
||||
},
|
||||
]);
|
||||
expect(records.listUnknowns()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("preserves registry failures rather than presenting a completed workflow", () => {
|
||||
const error = new UnknownRegistryError("limit");
|
||||
expect(
|
||||
recordWorkflowUnknowns({
|
||||
name: "trace_feature",
|
||||
result: { residual_unknowns: ["Which branch handles the request?"] },
|
||||
evidenceId: "unretained",
|
||||
recordUnknown: () => err(error),
|
||||
}),
|
||||
).toEqual(err(error));
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,8 @@ import {
|
|||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { UnknownRegistryPort } from "./investigation/InvestigationRecordPort.js";
|
||||
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
|
||||
|
||||
/** Build the shared Evidence record for a successful composed binary workflow. */
|
||||
export const createWorkflowEvidence = (input: {
|
||||
|
|
@ -62,3 +64,64 @@ export const workflowSnapshotRecord = (
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface WorkflowUnknownInput {
|
||||
readonly name: string;
|
||||
readonly result: JsonValue;
|
||||
readonly evidenceId: string;
|
||||
readonly recordUnknown: UnknownRegistryPort["recordUnknown"] | undefined;
|
||||
}
|
||||
|
||||
/** Retain residual workflow questions using the shared investigation policy. */
|
||||
export const recordWorkflowUnknowns = ({
|
||||
name,
|
||||
result,
|
||||
evidenceId,
|
||||
recordUnknown,
|
||||
}: WorkflowUnknownInput):
|
||||
| ReturnType<UnknownRegistryPort["recordUnknown"]>
|
||||
| { readonly ok: true; readonly value: null } => {
|
||||
if (
|
||||
!["trace_feature", "trace_call_path", "inspect_native_api"].includes(
|
||||
name,
|
||||
) ||
|
||||
recordUnknown === undefined ||
|
||||
typeof result !== "object" ||
|
||||
result === null ||
|
||||
Array.isArray(result) ||
|
||||
!Array.isArray(result.residual_unknowns)
|
||||
)
|
||||
return { ok: true, value: null };
|
||||
for (const question of result.residual_unknowns) {
|
||||
if (typeof question !== "string") continue;
|
||||
const recorded = recordUnknown({
|
||||
question,
|
||||
severity: "medium",
|
||||
domain: name === "inspect_native_api" ? "native-api" : "control-flow",
|
||||
supporting_evidence_ids: [evidenceId],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "shipped-artifact",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [
|
||||
{
|
||||
operation: name,
|
||||
rationale:
|
||||
name === "inspect_native_api"
|
||||
? "Confirm the unsupported boundary with a capable provider or ABI probe."
|
||||
: "Continue with a focused query or another available provider.",
|
||||
},
|
||||
],
|
||||
relationships: [],
|
||||
});
|
||||
if (
|
||||
!recorded.ok &&
|
||||
!(
|
||||
recorded.error instanceof UnknownRegistryError &&
|
||||
recorded.error.reason === "already-exists"
|
||||
)
|
||||
)
|
||||
return recorded;
|
||||
}
|
||||
return { ok: true, value: null };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import {
|
|||
import {
|
||||
createWorkflowEvidence,
|
||||
workflowSnapshotRecord,
|
||||
recordWorkflowUnknowns,
|
||||
} from "../application/WorkflowEvidence.js";
|
||||
import { toolContract, type ToolContract } from "../contracts/toolContracts.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { mcpProgressReporter } from "./mcpProgress.js";
|
||||
|
|
@ -324,63 +324,3 @@ const jsonParameters = (
|
|||
(entry): entry is [string, JsonValue] => entry[1] !== undefined,
|
||||
),
|
||||
);
|
||||
|
||||
interface WorkflowUnknownInput {
|
||||
readonly name: string;
|
||||
readonly result: JsonValue;
|
||||
readonly evidenceId: string;
|
||||
readonly recordUnknown: UnknownRegistryPort["recordUnknown"] | undefined;
|
||||
}
|
||||
|
||||
const recordWorkflowUnknowns = ({
|
||||
name,
|
||||
result,
|
||||
evidenceId,
|
||||
recordUnknown,
|
||||
}: WorkflowUnknownInput):
|
||||
| ReturnType<UnknownRegistryPort["recordUnknown"]>
|
||||
| { readonly ok: true; readonly value: null } => {
|
||||
if (
|
||||
!["trace_feature", "trace_call_path", "inspect_native_api"].includes(
|
||||
name,
|
||||
) ||
|
||||
recordUnknown === undefined ||
|
||||
typeof result !== "object" ||
|
||||
result === null ||
|
||||
Array.isArray(result) ||
|
||||
!Array.isArray(result.residual_unknowns)
|
||||
)
|
||||
return { ok: true, value: null };
|
||||
for (const question of result.residual_unknowns) {
|
||||
if (typeof question !== "string") continue;
|
||||
const recorded = recordUnknown({
|
||||
question,
|
||||
severity: "medium",
|
||||
domain: name === "inspect_native_api" ? "native-api" : "control-flow",
|
||||
supporting_evidence_ids: [evidenceId],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "shipped-artifact",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [
|
||||
{
|
||||
operation: name,
|
||||
rationale:
|
||||
name === "inspect_native_api"
|
||||
? "Confirm the unsupported boundary with a capable provider or ABI probe."
|
||||
: "Continue with a focused query or another available provider.",
|
||||
},
|
||||
],
|
||||
relationships: [],
|
||||
});
|
||||
if (
|
||||
!recorded.ok &&
|
||||
!(
|
||||
recorded.error instanceof UnknownRegistryError &&
|
||||
recorded.error.reason === "already-exists"
|
||||
)
|
||||
)
|
||||
return recorded;
|
||||
}
|
||||
return { ok: true, value: null };
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue