Summary
Canonical repository paths are constrained throughout the completed-scan contract, but coverage.deferred[].paths is currently only an array of non-empty strings.
A sealed coverage document can therefore describe deferred in-scope work using traversal, absolute, Windows-style, colon-bearing, standalone-dot, or NUL-bearing paths even though manifest scope paths, finding locations, artifact paths, and receipt references reject those forms.
Reproduction / evidence
Current upstream main at 37bf87a692fc72d41f7312cc48808d699d204fba documents each deferred unit as a stable id/reason with optional paths or surfaceIds.
The shared coverage.schema.json defines each deferred path as only:
{
"type": "string",
"minLength": 1
}
_validate_coverage() in finalize_scan_contract.py validates surface receipt paths, but does not run deferred paths through _require_safe_relative_path() before the shared schema validation.
A deterministic sealed-contract reproduction is:
- copy the bundled
examples/completed-scan fixture;
- set coverage completeness to
partial and add a deferred unit with paths: ["../../outside.ts"];
- update the sealed
coverage.json digest in scan-manifest.json;
- call
loadContract().
Current behavior: the traversal path passes the coverage schema and the sealed bundle loads.
Expected behavior: documented deferred source paths should obey the same repository-relative POSIX boundary as other canonical source paths.
Root cause
The deferred-path field was added to the coverage schema without the safe-path constraint already used for canonical source/path-bearing fields.
Suggested fix
Apply the canonical safe repository-relative pattern to deferred[].paths, rejecting:
- absolute paths;
.. traversal segments;
- backslashes / Windows-style paths;
- colon-bearing paths;
- standalone
.;
- NUL characters.
Because the coverage schema is consumed by both finalization and the TypeScript contract loader, one schema change can enforce the invariant on both producer and consumer surfaces.
Add a focused sealed-contract regression that reseals a partial coverage fixture after inserting invalid paths and verifies loadContract() rejects them, with a normal repository-relative path as the control.
Impact
This is contract-integrity correctness. Deferred paths do not cause an arbitrary file read by themselves, but a sealed canonical bundle can currently attribute unreviewed work to a path outside the repository path model enforced everywhere else, which can mislead downstream coverage/reporting consumers.
Summary
Canonical repository paths are constrained throughout the completed-scan contract, but
coverage.deferred[].pathsis currently only an array of non-empty strings.A sealed coverage document can therefore describe deferred in-scope work using traversal, absolute, Windows-style, colon-bearing, standalone-dot, or NUL-bearing paths even though manifest scope paths, finding locations, artifact paths, and receipt references reject those forms.
Reproduction / evidence
Current upstream
mainat37bf87a692fc72d41f7312cc48808d699d204fbadocuments each deferred unit as a stable id/reason with optionalpathsorsurfaceIds.The shared
coverage.schema.jsondefines each deferred path as only:{ "type": "string", "minLength": 1 }_validate_coverage()infinalize_scan_contract.pyvalidates surface receipt paths, but does not run deferred paths through_require_safe_relative_path()before the shared schema validation.A deterministic sealed-contract reproduction is:
examples/completed-scanfixture;partialand add a deferred unit withpaths: ["../../outside.ts"];coverage.jsondigest inscan-manifest.json;loadContract().Current behavior: the traversal path passes the coverage schema and the sealed bundle loads.
Expected behavior: documented deferred source paths should obey the same repository-relative POSIX boundary as other canonical source paths.
Root cause
The deferred-path field was added to the coverage schema without the safe-path constraint already used for canonical source/path-bearing fields.
Suggested fix
Apply the canonical safe repository-relative pattern to
deferred[].paths, rejecting:..traversal segments;.;Because the coverage schema is consumed by both finalization and the TypeScript contract loader, one schema change can enforce the invariant on both producer and consumer surfaces.
Add a focused sealed-contract regression that reseals a partial coverage fixture after inserting invalid paths and verifies
loadContract()rejects them, with a normal repository-relative path as the control.Impact
This is contract-integrity correctness. Deferred paths do not cause an arbitrary file read by themselves, but a sealed canonical bundle can currently attribute unreviewed work to a path outside the repository path model enforced everywhere else, which can mislead downstream coverage/reporting consumers.