fix(#1907): validate per-item shape in isValidReport so adapter garbage fails closed (#1910)

* fix(#1907): validate per-item shape in isValidReport so adapter garbage fails closed

isValidReport checked only the container (items is-array, coverage scalars), so a report
like {items:[{}]} sailed through runProbeCli and stringified as green output — despite the
docstring promising it 'fails closed on adapter garbage'. Add a per-item Item-contract guard
(requirement_id/category/status + typed nullable fields) so a future adapter that bypasses
the analyzeCoverage merge and returns per-item garbage inside a well-shaped envelope fails
closed (exit 2) instead of emitting it as valid coverage.

analyzeCoverage always emits fully-populated, validated Items, so the 2 shipped adapters are
unaffected (verified across the edge/prohibition suites).

Refs #1907, epic #1904.

* chore(changeset): Fixed fragment for #1910 (isValidReport per-item)
This commit is contained in:
Rezolv
2026-07-02 11:55:53 -04:00
committed by GitHub
parent a4bc04a5ff
commit 312c9d3ec3
3 changed files with 57 additions and 1 deletions

View File

@@ -96,16 +96,42 @@ function errMessage(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
/**
* Structural guard for ONE item of the report `items[]` — enforces the `Item` contract (#1907).
* `analyzeCoverage` always emits fully-populated, category/status-validated Items, so this never
* rejects legitimate output; it catches an adapter that bypasses the merge and hands back per-item
* garbage (e.g. `items:[{}]`) inside a well-shaped envelope — which the container-only guard let
* sail through as green output despite the fail-closed docstring below.
*/
function isValidItem(item: unknown): item is Item {
if (item == null || typeof item !== 'object') return false;
const i = item as {
requirement_id?: unknown; category?: unknown; status?: unknown;
verification?: unknown; resolution?: unknown; reason?: unknown; probe?: unknown;
};
if (typeof i.requirement_id !== 'string' || !i.requirement_id.trim()) return false;
if (typeof i.category !== 'string' || !i.category.trim()) return false;
if (!VALID_STATUS.includes(i.status as Status)) return false;
if (typeof i.probe !== 'string') return false;
// The three nullable fields must be a string or null — never some other type.
if (i.verification !== null && typeof i.verification !== 'string') return false;
if (i.resolution !== null && typeof i.resolution !== 'string') return false;
if (i.reason !== null && typeof i.reason !== 'string') return false;
return true;
}
/**
* Structural guard for the report an adapter's `analyze` returns. The scaffold types `analyze`
* loosely (it runs over JSON-parsed input the adapter `as`-casts), so a future adapter (#644)
* that forgets to validate inside its closure could hand back a malformed object. Rather than
* stringify garbage as green output, `runProbeCli` checks the report shape and fails closed.
* stringify garbage as green output, `runProbeCli` checks the report shape — container AND every
* item — and fails closed.
*/
function isValidReport(report: unknown): report is CoverageReport {
if (report == null || typeof report !== 'object') return false;
const r = report as { items?: unknown; coverage?: unknown };
if (!Array.isArray(r.items)) return false;
if (!r.items.every(isValidItem)) return false;
const c = r.coverage as
| { applicable?: unknown; resolved?: unknown; unresolved?: unknown; byVerification?: unknown }
| undefined;