fix(reconstruction): evaluate one effective completion scope

Include required-claim surfaces and reachable dependencies in ownership, verifier commitments, contradiction, package proof and summary checks. Preserve all applicable package outcomes instead of accepting any passing proof.

Adds authenticated synthetic scope cases and a shared public CLI/MCP regression workflow. Validation is pending the centrally coordinated lane; single-file formatting and lint completed.
This commit is contained in:
morluto
2026-10-24 02:29:08 +08:00
parent 8425c36da2
commit a35b9335a0
7 changed files with 386 additions and 42 deletions
+23
View File
@@ -11,6 +11,23 @@ record can still be insufficient proof: a passing verifier summary cannot
promote inferred Evidence into a replay observation. That case returns
`unknown`, while a missing or tampered record remains an input error.
## Effective completion scope
The selected boundary requires the union of its explicit surface IDs, the
surfaces of every required claim, and every declared dependency reachable from
those surfaces. Cycles are visited once. Missing reachable dependencies remain
visible as `dependency-surface-missing`; they keep closure partial. The
`required_surfaces` summary counts this complete set, including absent IDs,
and `required_claims` counts unique required claims.
Every reachable surface receives the same ownership, path, distribution,
disposition and authority-independence checks. Active contradictions on any of
these surfaces affect the boundary even when the caller did not repeat the
surface ID in `required_surface_ids`. Package proof must cover the artifacts of
this effective scope. A claim verifier must commit to its own surfaces and
their reachable dependencies, including their artifact and implemented-owner
digests. Callers do not need to duplicate the transitive closure in the request.
## Comparable verifier Evidence
At least one record explicitly cited by the latest verifier observation must
@@ -41,3 +58,9 @@ neither a passing nor a failed package workflow.
The evaluator preserves Evidence provenance and returns an actionable unknown
when qualification is unavailable. It does not change the cited records to
make their authority match a caller's declaration.
All applicable package-proof outcomes participate. A failed applicable proof
fails closure even when another proof passes; unknown, unsupported or skipped
applicable outcomes keep it unknown. The result cites all applicable outcomes.
Proof array order does not establish freshness or supersession. A proof for
unrelated artifact digests does not contradict the selected boundary.
+26
View File
@@ -1,3 +1,4 @@
import { reconstructionCoverageScopeCases } from "./reconstructionCoverageScope.fixture.js";
import { describe, expect, it } from "vitest";
import {
@@ -608,3 +609,28 @@ describe("reconstruction coverage reference constraints", () => {
},
);
});
describe("effective reconstruction boundary", () => {
for (const {
label,
coverage,
status,
codes,
surfaces,
evidenceIds,
} of reconstructionCoverageScopeCases())
it(label, () => {
const result = assertReconstructionCoverageEvaluation(
coverage,
"replacement.cli",
RECONSTRUCTION_COVERAGE_NOW,
);
expect(result.status).toBe(status);
expect(result.summary.required_surfaces).toBe(surfaces);
expect(result.reasons.map(({ code }) => code).sort()).toEqual(
[...codes].sort(),
);
expect(result.evidence_ids).toEqual(expect.arrayContaining(evidenceIds));
expect(result.recommended_probes).toHaveLength(codes.length);
});
});
+4 -2
View File
@@ -1,3 +1,4 @@
import { reconstructionCoverageScope } from "./reconstructionCoverageScope.js";
import { z } from "zod";
import { AnalysisInputError } from "./analysisErrorCore.js";
import type { EvidenceIntegrityError } from "./evidenceErrors.js";
@@ -345,6 +346,7 @@ export const evaluateParsedReconstructionClosure = (
const context: ReconstructionEvaluationContext = {
...parsed,
boundary,
scope: reconstructionCoverageScope(parsed, boundary),
nowEpochMs,
nowInstant,
reasons: [],
@@ -360,8 +362,8 @@ export const evaluateParsedReconstructionClosure = (
boundary_id: boundary.boundary_id,
status: closureStatus(reasons),
summary: {
required_surfaces: boundary.required_surface_ids.length,
required_claims: boundary.required_claim_ids.length,
required_surfaces: context.scope.surfaceIds.size,
required_claims: context.scope.claimIds.size,
reasons: reasons.length,
},
reasons,
+47 -40
View File
@@ -1,3 +1,4 @@
import type { ReconstructionCoverageScope } from "./reconstructionCoverageScope.js";
import type {
ReconstructionClosureResult,
ReconstructionCoverageData,
@@ -42,6 +43,7 @@ type Boundary = ReconstructionCoverageData["boundaries"][number];
type ClosureReason = ReconstructionClosureResult["reasons"][number];
export interface ReconstructionEvaluationContext extends ParsedReconstructionCoverage {
readonly boundary: Boundary;
readonly scope: ReconstructionCoverageScope;
readonly nowEpochMs: number;
readonly nowInstant: OffsetDateTimeInstant;
readonly reasons: ClosureReason[];
@@ -51,26 +53,28 @@ export interface ReconstructionEvaluationContext extends ParsedReconstructionCov
export const evaluateReconstructionSurfaces = (
context: ReconstructionEvaluationContext,
): void => {
for (const surfaceId of context.boundary.required_surface_ids) {
for (const surfaceId of context.scope.surfaceIds) {
const surface = context.surfaces.get(surfaceId);
if (surface === undefined) {
addReason(
context,
"surface-missing",
surfaceId,
"Required surface is absent from the authoritative inventory.",
);
const dependents = context.scope.missingDependencies.get(surfaceId);
if (dependents === undefined)
addReason(
context,
"surface-missing",
surfaceId,
"Required surface is absent from the authoritative inventory.",
);
else
for (const dependentId of dependents)
addReason(
context,
"dependency-surface-missing",
dependentId,
`Declared dependency surface is absent: ${surfaceId}`,
);
continue;
}
addEvidence(context, surface.evidence_ids);
for (const dependencyId of surface.dependency_surface_ids)
if (!context.surfaces.has(dependencyId))
addReason(
context,
"dependency-surface-missing",
surfaceId,
`Declared dependency surface is absent: ${dependencyId}`,
);
evaluateOwner(context, surfaceId);
}
};
@@ -152,7 +156,7 @@ export const evaluateReconstructionClaims = (
context: ReconstructionEvaluationContext,
): void => {
const indexes = createVerifierIndexes(context);
for (const claimId of context.boundary.required_claim_ids) {
for (const claimId of context.scope.claimIds) {
const claim = context.claims.get(claimId);
if (claim === undefined) {
addReason(
@@ -189,6 +193,7 @@ export const evaluateReconstructionClaims = (
};
interface VerifierIndexes {
readonly claimSurfaceIds: ReconstructionCoverageScope["claimSurfaceIds"];
readonly contractsByClaim: ReadonlyMap<
string,
readonly ReconstructionVerifierContract[]
@@ -226,6 +231,7 @@ const createVerifierIndexes = (
}
return {
claimSurfaceIds: context.scope.claimSurfaceIds,
contractsByClaim,
latestResultsByVerifier: latestVerifierObservationsByVerifier(
coverage.verifier_results,
@@ -342,7 +348,10 @@ const verifierResultMatchesContract = (
result: ReconstructionCoverageData["verifier_results"][number],
indexes: VerifierIndexes,
): boolean => {
const expectedArtifactDigests = claim.surface_ids.flatMap((surfaceId) => {
const requiredSurfaceIds = [
...(indexes.claimSurfaceIds.get(claim.claim_id) ?? []),
];
const expectedArtifactDigests = requiredSurfaceIds.flatMap((surfaceId) => {
const surface = indexes.surfaces.get(surfaceId);
const artifact =
surface === undefined
@@ -350,7 +359,7 @@ const verifierResultMatchesContract = (
: indexes.artifacts.get(surface.artifact_id);
return artifact === undefined ? [] : [artifact.artifact_sha256];
});
const expectedOwnerDigests = claim.surface_ids.flatMap((surfaceId) =>
const expectedOwnerDigests = requiredSurfaceIds.flatMap((surfaceId) =>
(indexes.ownersBySurface.get(surfaceId) ?? []).flatMap(({ ownership }) =>
ownership.disposition === "implemented" ? [ownership.owner_sha256] : [],
),
@@ -399,11 +408,9 @@ export const evaluateReconstructionCoverageRisks = (
if (
contradiction.status === "active" &&
(contradiction.surface_ids.some((id) =>
context.boundary.required_surface_ids.includes(id),
context.scope.surfaceIds.has(id),
) ||
contradiction.claim_ids.some((id) =>
context.boundary.required_claim_ids.includes(id),
))
contradiction.claim_ids.some((id) => context.scope.claimIds.has(id)))
) {
addEvidence(context, contradiction.evidence_ids);
addReason(
@@ -418,7 +425,7 @@ export const evaluateReconstructionCoverageRisks = (
export const evaluateReconstructionPackageProofs = (
context: ReconstructionEvaluationContext,
): void => {
const requiredArtifactDigests = context.boundary.required_surface_ids.flatMap(
const requiredArtifactDigests = [...context.scope.surfaceIds].flatMap(
(surfaceId) => {
const surface = context.surfaces.get(surfaceId);
const artifact =
@@ -438,34 +445,34 @@ export const evaluateReconstructionPackageProofs = (
requiredArtifactDigests.every((digest) =>
proof.artifact_sha256s.includes(digest),
) && packageProofHasComparableEvidence(proof, context.evidenceById);
const passing = proofs.find(
(proof) => proof.status === "pass" && applicable(proof),
const applicableProofs = proofs.filter(applicable);
addEvidence(
context,
applicableProofs.flatMap((proof) => proof.evidence_ids),
);
if (passing !== undefined) {
addEvidence(context, passing.evidence_ids);
continue;
}
const failing = proofs.find(
(proof) => proof.status === "fail" && applicable(proof),
);
if (failing !== undefined) {
addEvidence(context, failing.evidence_ids);
if (applicableProofs.some((proof) => proof.status === "fail")) {
addReason(
context,
"package-proof-failed",
kind,
"Required package or integration proof failed.",
"Required package or integration proof failed; all applicable outcomes are retained.",
);
} else if (
applicableProofs.length > 0 &&
applicableProofs.every((proof) => proof.status === "pass")
) {
continue;
} else if (proofs.length > 0) {
addEvidence(
context,
proofs.flatMap(({ evidence_ids }) => evidence_ids),
);
if (applicableProofs.length === 0)
addEvidence(
context,
proofs.flatMap((proof) => proof.evidence_ids),
);
addReason(
context,
"package-proof-unknown",
kind,
"Required package or integration proof has no qualifying passing Evidence.",
"Required package or integration proof has unresolved or unqualified outcomes.",
);
} else
addReason(
@@ -0,0 +1,184 @@
import { createEvidence } from "./evidence.js";
import { createEvidenceBundle } from "./evidenceBundle.js";
import {
completeReconstructionCoverageData,
reconstructionCoverageEvidenceId,
} from "./reconstructionCoverage.fixture.js";
import type {
ReconstructionCoverageData,
ReconstructionClosureResult,
} from "./reconstructionCoverage.js";
const indirectSurface = (coverage: ReconstructionCoverageData): void => {
coverage.surfaces.push({
...coverage.surfaces[0]!,
surface_id: "cli.indirect",
});
coverage.owners.push({ ...coverage.owners[0]!, surface_id: "cli.indirect" });
};
const conflictingPackageProof = (
coverage: ReconstructionCoverageData,
status: "fail" | "unknown",
): string => {
const evidence = createEvidence(
undefined,
{
id: "package-proof-fixture",
name: "Package proof fixture",
version: "1",
},
{
operation: "record_package_proof",
parameters: {},
result: { status },
authority: "controlled-replay",
confidence: "observed",
},
);
coverage.evidence_bundle = createEvidenceBundle([
...coverage.evidence_bundle.records,
evidence,
]);
coverage.package_proofs.push({
...coverage.package_proofs[0]!,
proof_id: `proof.additional.${status}`,
status,
evidence_ids: [evidence.evidence_id],
});
return evidence.evidence_id;
};
const cases: {
label: string;
status: ReconstructionClosureResult["status"];
codes: readonly string[];
surfaces: number;
change: (coverage: ReconstructionCoverageData) => readonly string[];
}[] = [
{
label: "complete control",
status: "ready",
codes: [],
surfaces: 1,
change: () => [],
},
{
label: "reachable dependency cycle",
status: "ready",
codes: [],
surfaces: 2,
change: (coverage) => {
indirectSurface(coverage);
coverage.surfaces[0]!.dependency_surface_ids = ["cli.indirect"];
coverage.surfaces[1]!.dependency_surface_ids = ["cli.help"];
return [];
},
},
{
label: "missing transitive dependency",
status: "partial",
codes: ["dependency-surface-missing"],
surfaces: 3,
change: (coverage) => {
indirectSurface(coverage);
coverage.surfaces[0]!.dependency_surface_ids = ["cli.indirect"];
coverage.surfaces[1]!.dependency_surface_ids = ["cli.absent"];
return [];
},
},
{
label: "claim-only authority routing",
status: "failed",
codes: ["authority-routing-detected"],
surfaces: 2,
change: (coverage) => {
indirectSurface(coverage);
coverage.claims[0]!.surface_ids = ["cli.indirect"];
const owner = coverage.owners[1]!.ownership;
if (owner.disposition === "implemented")
owner.authority_route = "detected";
return [];
},
},
{
label: "claim-only active contradiction",
status: "failed",
codes: ["active-contradiction"],
surfaces: 2,
change: (coverage) => {
indirectSurface(coverage);
coverage.claims[0]!.surface_ids = ["cli.indirect"];
const evidenceIds = [
reconstructionCoverageEvidenceId("1"),
reconstructionCoverageEvidenceId("5"),
];
coverage.contradictions = [
{
contradiction_id: "contradiction.indirect",
status: "active",
surface_ids: ["cli.indirect"],
claim_ids: [],
evidence_ids: evidenceIds,
},
];
return evidenceIds;
},
},
{
label: "claim-only artifact package coverage",
status: "unknown",
codes: ["package-proof-unknown", "package-proof-unknown"],
surfaces: 2,
change: (coverage) => {
indirectSurface(coverage);
coverage.artifacts.push({
...coverage.artifacts[0]!,
artifact_id: "authority.v2",
artifact_sha256: "9".repeat(64),
});
coverage.surfaces[1]!.artifact_id = "authority.v2";
coverage.claims[0]!.surface_ids = ["cli.indirect"];
coverage.verifier_results[0]!.artifact_sha256s.push("9".repeat(64));
return [];
},
},
{
label: "transitive owner commitment",
status: "unknown",
codes: ["verifier-result-incompatible"],
surfaces: 2,
change: (coverage) => {
indirectSurface(coverage);
coverage.surfaces[0]!.dependency_surface_ids = ["cli.indirect"];
const owner = coverage.owners[1]!.ownership;
if (owner.disposition === "implemented")
owner.owner_sha256 = "9".repeat(64);
return [];
},
},
...([false, true] as const).map((reverse) => ({
label: `conflicting package proofs ${reverse ? "reversed" : "forward"}`,
status: "failed" as const,
codes: ["package-proof-failed"],
surfaces: 1,
change: (coverage: ReconstructionCoverageData) => {
const evidenceId = conflictingPackageProof(coverage, "fail");
if (reverse) coverage.package_proofs.reverse();
return [evidenceId];
},
})),
{
label: "unresolved applicable package proof",
status: "unknown",
codes: ["package-proof-unknown"],
surfaces: 1,
change: (coverage) => [conflictingPackageProof(coverage, "unknown")],
},
];
/** Small authenticated synthetic workspaces shared by domain and public workflows. */
export const reconstructionCoverageScopeCases = () =>
cases.map(({ change, ...expectation }) => {
const coverage = completeReconstructionCoverageData();
return { ...expectation, coverage, evidenceIds: change(coverage) };
});
+64
View File
@@ -0,0 +1,64 @@
import type {
ParsedReconstructionCoverage,
ReconstructionCoverageData,
} from "./reconstructionCoverage.js";
/** One completion scope shared by ownership, proof, risk and summary checks. */
export interface ReconstructionCoverageScope {
readonly surfaceIds: ReadonlySet<string>;
readonly claimIds: ReadonlySet<string>;
readonly claimSurfaceIds: ReadonlyMap<string, ReadonlySet<string>>;
readonly missingDependencies: ReadonlyMap<string, ReadonlySet<string>>;
}
export const reconstructionCoverageScope = (
parsed: ParsedReconstructionCoverage,
boundary: ReconstructionCoverageData["boundaries"][number],
): ReconstructionCoverageScope => {
const claimIds = new Set(boundary.required_claim_ids);
const claimSurfaceIds = new Map<string, ReadonlySet<string>>();
for (const claimId of claimIds) {
const claim = parsed.claims.get(claimId);
if (claim !== undefined)
claimSurfaceIds.set(
claimId,
reachableSurfaces(claim.surface_ids, parsed.surfaces).surfaceIds,
);
}
return {
...reachableSurfaces(
[
...boundary.required_surface_ids,
...[...claimSurfaceIds.values()].flatMap((ids) => [...ids]),
],
parsed.surfaces,
),
claimIds,
claimSurfaceIds,
};
};
const reachableSurfaces = (
roots: readonly string[],
surfaces: ParsedReconstructionCoverage["surfaces"],
): Pick<ReconstructionCoverageScope, "surfaceIds" | "missingDependencies"> => {
const surfaceIds = new Set(roots);
const pending = [...surfaceIds];
const missingDependencies = new Map<string, Set<string>>();
for (let index = 0; index < pending.length; index += 1) {
const surface = surfaces.get(pending[index]!);
if (surface === undefined) continue;
for (const dependencyId of surface.dependency_surface_ids) {
if (!surfaces.has(dependencyId)) {
const dependents = missingDependencies.get(dependencyId) ?? new Set();
dependents.add(surface.surface_id);
missingDependencies.set(dependencyId, dependents);
}
if (!surfaceIds.has(dependencyId)) {
surfaceIds.add(dependencyId);
pending.push(dependencyId);
}
}
}
return { surfaceIds, missingDependencies };
};
@@ -1,3 +1,5 @@
import { reconstructionClosureResultSchema } from "../../../src/domain/reconstructionCoverage.js";
import { reconstructionCoverageScopeCases } from "../../../src/domain/reconstructionCoverageScope.fixture.js";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
@@ -115,3 +117,39 @@ cliTest(
});
},
);
cliTest(
"accounts for effective scope and conflicting proofs through CLI and MCP",
async ({ cli }) => {
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Expected shared file MCP session");
for (const {
label,
coverage,
status,
codes,
surfaces,
evidenceIds,
} of reconstructionCoverageScopeCases()) {
coverage.verifier_results[0]!.observed_at = new Date().toISOString();
const cliResult = await evaluate(cli, coverage);
expect(cliResult.exitCode, label).toBe(0);
const mcpResult = await call("evaluate_reconstruction_coverage", {
coverage,
boundary_id: "replacement.cli",
});
expect(mcpResult.isError, label).not.toBe(true);
for (const output of [cliResult.json, mcpResult.structuredContent]) {
const result = reconstructionClosureResultSchema.parse(output);
expect(result.status, label).toBe(status);
expect(result.summary.required_surfaces, label).toBe(surfaces);
expect(result.reasons.map(({ code }) => code).sort(), label).toEqual(
[...codes].sort(),
);
expect(result.evidence_ids, label).toEqual(
expect.arrayContaining(evidenceIds),
);
}
}
},
);