diff --git a/src/matrix/resolver.mjs b/src/matrix/resolver.mjs index b7906c4..658d04c 100644 --- a/src/matrix/resolver.mjs +++ b/src/matrix/resolver.mjs @@ -48,6 +48,7 @@ export function resolveCoverageObligations({ profile, entries, surfaces, targetP const stateResult = stateSatisfiesRequirement(state, requirement); const targetResult = targetSatisfiesRequirement(targetPlan, requirement); + warnings.push(...legacyCompatibilityWarnings({ entry, surface, requirement })); const status = entry.skipReason ? "skipped" : !stateResult.ok @@ -80,6 +81,49 @@ export function resolveCoverageObligations({ profile, entries, surfaces, targetP }; } +function legacyCompatibilityWarnings({ entry, surface, requirement }) { + const warnings = []; + const scenario = entry.scenario; + const state = entry.state; + if (!scenario || !state || !surface || !requirement) { + return warnings; + } + + if ((state.compatibleSurfaces ?? []).length > 0 && !state.compatibleSurfaces.includes(surface.id)) { + warnings.push({ + kind: "legacy-compatibility-disagreement", + surface: surface.id, + scenario: scenario.id, + state: state.id, + requirement: requirement.id, + message: `state '${state.id}' does not list surface '${surface.id}' in compatibleSurfaces` + }); + } + if ((state.incompatibleSurfaces ?? []).includes(surface.id)) { + warnings.push({ + kind: "legacy-compatibility-disagreement", + surface: surface.id, + scenario: scenario.id, + state: state.id, + requirement: requirement.id, + message: `state '${state.id}' lists surface '${surface.id}' in incompatibleSurfaces` + }); + } + if ((surface.requiredStates ?? []).length > 0 && + (scenario.states ?? []).length === 0 && + !surface.requiredStates.includes(state.id)) { + warnings.push({ + kind: "legacy-compatibility-disagreement", + surface: surface.id, + scenario: scenario.id, + state: state.id, + requirement: requirement.id, + message: `surface '${surface.id}' requiredStates does not include state '${state.id}'` + }); + } + return warnings; +} + export function assertResolvedCoverageIsRunnable(resolved) { const invalid = (resolved?.obligations ?? []).filter((obligation) => ["invalid", "missing-proof", "unsupported-state", "unsupported-target"].includes(obligation.status) diff --git a/src/selfcheck.mjs b/src/selfcheck.mjs index e6364e2..b7dcdd0 100644 --- a/src/selfcheck.mjs +++ b/src/selfcheck.mjs @@ -148,6 +148,7 @@ export async function runSelfCheck(flags = {}) { assertArrayNotEmpty(data.entries, "matrix entries"); assertEqual(data.resolvedCoverage?.schemaVersion, "kova.resolvedCoverage.v1", "resolved coverage schema"); assertEqual(data.resolvedCoverage?.statuses?.planned, 1, "resolved planned obligation count"); + assertEqual(data.resolvedCoverage?.warnings?.length, 0, "resolved coverage migration warnings"); assertEqual(data.resolvedCoverage?.obligations?.[0]?.surface, "fresh-install", "resolved obligation surface"); assertEqual(data.resolvedCoverage?.obligations?.[0]?.requirement, "baseline", "resolved obligation requirement"); assertEqual(data.entries.length, 1, "matrix include filter count"); @@ -5171,6 +5172,7 @@ function validateReport(report) { assertEqual(report.schemaVersion, "kova.report.v1", "report schema"); assertEqual(report.mode, "dry-run", "report mode"); assertEqual(report.summary?.statuses?.["DRY-RUN"], 2, "report dry-run count"); + assertEqual(Object.hasOwn(report, "resolvedCoverage"), false, "report does not include planner-only resolved coverage"); assertEqual(report.performance?.repeat, 2, "report repeat count"); assertEqual(report.performance?.groupCount, 1, "report performance group count"); assertArrayNotEmpty(report.records, "report records");