Hint if version is not an incremental update (#3840)

diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart
index 9a5e214..c367827 100644
--- a/lib/src/validator/relative_version_numbering.dart
+++ b/lib/src/validator/relative_version_numbering.dart
@@ -5,12 +5,16 @@
 import 'dart:async';
 
 import 'package:collection/collection.dart' show IterableExtension;
+import 'package:pub_semver/pub_semver.dart';
 
 import '../exceptions.dart';
 import '../package_name.dart';
 import '../validator.dart';
 
-/// Gives a warning when publishing a new version, if the latest published
+/// Gives an info if the version number has skipped since the last released, or
+/// if the version is not sequentially following the latest.
+///
+/// Gives an info when publishing a new version, if the latest published
 /// version lower to this was not opted into null-safety.
 class RelativeVersionNumberingValidator extends Validator {
   static const String semverUrl =
@@ -31,11 +35,57 @@
       existingVersions = [];
     }
     existingVersions.sort((a, b) => a.version.compareTo(b.version));
-    final previousVersion = existingVersions
-        .lastWhereOrNull((id) => id.version < entrypoint.root.version);
-    if (previousVersion == null) return;
 
-    final previousPubspec = await entrypoint.cache.describe(previousVersion);
+    final currentVersion = entrypoint.root.pubspec.version;
+
+    final latestVersion =
+        existingVersions.isEmpty ? null : existingVersions.last.version;
+    if (latestVersion != null && latestVersion > currentVersion) {
+      hints.add('''
+The latest published version is $latestVersion.
+Your version $currentVersion is earlier than that.''');
+    }
+
+    final previousRelease = existingVersions
+        .lastWhereOrNull((id) => id.version < entrypoint.root.version);
+
+    if (previousRelease == null) return;
+
+    final previousVersion = previousRelease.version;
+    final noPrerelease = Version(
+      currentVersion.major,
+      currentVersion.minor,
+      currentVersion.patch,
+    );
+    if (noPrerelease != previousVersion.nextMajor &&
+        noPrerelease != previousVersion.nextMinor &&
+        noPrerelease != previousVersion.nextPatch &&
+        currentVersion.withoutBuild() != previousVersion) {
+      final hint = '''
+The previous version is $previousVersion.
+
+It seems you are not publishing an incremental update.
+
+Consider one of:
+''';
+      final String suggestion;
+
+      if (previousVersion.major == 0) {
+        suggestion = '''
+* ${previousVersion.nextMajor} for a first major release.
+* ${previousVersion.nextBreaking} for a breaking release.
+* ${previousVersion.nextPatch} for a minor release.
+''';
+      } else {
+        suggestion = '''
+* ${previousVersion.nextBreaking} for a breaking release.
+* ${previousVersion.nextMinor} for a minor release.
+* ${previousVersion.nextPatch} for a patch release.''';
+      }
+      hints.add(hint + suggestion);
+    }
+
+    final previousPubspec = await entrypoint.cache.describe(previousRelease);
 
     final currentOptedIn =
         entrypoint.root.pubspec.languageVersion.supportsNullSafety;
@@ -44,14 +94,19 @@
     if (currentOptedIn && !previousOptedIn) {
       hints.add(
           'You\'re about to publish a package that opts into null safety.\n'
-          'The previous version (${previousVersion.version}) isn\'t opted in.\n'
+          'The previous version ($previousVersion) isn\'t opted in.\n'
           'See $nullSafetyGuideUrl for best practices.');
     } else if (!currentOptedIn && previousOptedIn) {
       hints.add(
           'You\'re about to publish a package that doesn\'t opt into null safety,\n'
-          'but the previous version (${previousVersion.version}) was opted in.\n'
+          'but the previous version ($previousVersion) was opted in.\n'
           'This change is likely to be backwards incompatible.\n'
           'See $semverUrl for information about versioning.');
     }
   }
 }
+
+extension on Version {
+  Version withoutBuild() =>
+      Version(major, minor, patch, pre: preRelease.join('.'));
+}
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 7f414cc..d41001e 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -35,11 +35,11 @@
     libPubspec('test_pkg', '1.0.0', sdk: '>=3.1.2 <=3.2.0', extras: extras);
 
 /// Describes a package that passes all validation.
-DirectoryDescriptor get validPackage => dir(appPath, [
-      validPubspec(),
+DirectoryDescriptor validPackage({String version = '1.0.0'}) => dir(appPath, [
+      validPubspec(extras: {'version': version}),
       file('LICENSE', 'Eh, do what you want.'),
       file('README.md', "This package isn't real."),
-      file('CHANGELOG.md', '# 1.0.0\nFirst version\n'),
+      file('CHANGELOG.md', '# $version\nFirst version\n'),
       dir('lib', [file('test_pkg.dart', 'int i = 1;')])
     ]);
 
diff --git a/test/directory_option_test.dart b/test/directory_option_test.dart
index 5bc1cfd..d585f0c 100644
--- a/test/directory_option_test.dart
+++ b/test/directory_option_test.dart
@@ -31,7 +31,7 @@
       },
     );
 
-    await validPackage.create();
+    await validPackage().create();
     await dir(appPath, [
       dir('bin', [
         file('app.dart', '''
diff --git a/test/lish/archives_and_uploads_a_package_test.dart b/test/lish/archives_and_uploads_a_package_test.dart
index dcd66cc..0e288e3 100644
--- a/test/lish/archives_and_uploads_a_package_test.dart
+++ b/test/lish/archives_and_uploads_a_package_test.dart
@@ -17,7 +17,7 @@
 void main() {
   test('archives and uploads a package', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
@@ -40,7 +40,7 @@
 
   test('archives and uploads a package using token', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
@@ -68,7 +68,7 @@
 
   test('publishes to hosted-url with path', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
@@ -105,7 +105,7 @@
   test('with an empty Git submodule', () async {
     await d.git('empty').create();
 
-    var repo = d.git(appPath, d.validPackage.contents);
+    var repo = d.git(appPath, d.validPackage().contents);
     await repo.create();
 
     await repo.runGit([
diff --git a/test/lish/cloud_storage_upload_doesnt_redirect_test.dart b/test/lish/cloud_storage_upload_doesnt_redirect_test.dart
index fd14f85..f95db5e 100644
--- a/test/lish/cloud_storage_upload_doesnt_redirect_test.dart
+++ b/test/lish/cloud_storage_upload_doesnt_redirect_test.dart
@@ -12,7 +12,7 @@
 void main() {
   test("cloud storage upload doesn't redirect", () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/cloud_storage_upload_provides_an_error_test.dart b/test/lish/cloud_storage_upload_provides_an_error_test.dart
index 78b783e..bd14d84 100644
--- a/test/lish/cloud_storage_upload_provides_an_error_test.dart
+++ b/test/lish/cloud_storage_upload_provides_an_error_test.dart
@@ -12,7 +12,7 @@
 void main() {
   test('cloud storage upload provides an error', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/dot_folder_name_test.dart b/test/lish/dot_folder_name_test.dart
index d709f77..b25ad80 100644
--- a/test/lish/dot_folder_name_test.dart
+++ b/test/lish/dot_folder_name_test.dart
@@ -11,7 +11,7 @@
 void main() {
   test('Can publish files in a .folder', () async {
     await d.git(appPath).create();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.dir(appPath, [
       d.dir('.vscode', [d.file('a')]),
       d.file('.pubignore', '!.vscode/')
diff --git a/test/lish/dry_run_package_validation_has_a_warning_test.dart b/test/lish/dry_run_package_validation_has_a_warning_test.dart
index d6c2166..358a986 100644
--- a/test/lish/dry_run_package_validation_has_a_warning_test.dart
+++ b/test/lish/dry_run_package_validation_has_a_warning_test.dart
@@ -12,7 +12,7 @@
 void main() {
   test('dry-run package validation gives a warning', () async {
     (await servePackages()).serve('foo', '1.0.0');
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     var pkg = packageMap(
       'test_pkg',
diff --git a/test/lish/dry_run_package_validation_has_no_warnings_test.dart b/test/lish/dry_run_package_validation_has_no_warnings_test.dart
index 13ed386..3045220 100644
--- a/test/lish/dry_run_package_validation_has_no_warnings_test.dart
+++ b/test/lish/dry_run_package_validation_has_no_warnings_test.dart
@@ -12,7 +12,7 @@
 void main() {
   test('--dry-run package validation on valid package has no warnings',
       () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await servePackages();
     var pub = await startPublish(globalServer, args: ['--dry-run']);
diff --git a/test/lish/dry_run_warns_about_server_checks.dart b/test/lish/dry_run_warns_about_server_checks.dart
index ad7abe0..889d47b 100644
--- a/test/lish/dry_run_warns_about_server_checks.dart
+++ b/test/lish/dry_run_warns_about_server_checks.dart
@@ -9,7 +9,7 @@
 
 void main() {
   test('--dry-run mentions that checks are not exhaustive', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await runPub(
       args: ['publish', '--dry-run'],
       output: contains('The server may enforce additional checks.'),
diff --git a/test/lish/force_cannot_be_combined_with_dry_run_test.dart b/test/lish/force_cannot_be_combined_with_dry_run_test.dart
index b82e7ce..bcd3a48 100644
--- a/test/lish/force_cannot_be_combined_with_dry_run_test.dart
+++ b/test/lish/force_cannot_be_combined_with_dry_run_test.dart
@@ -10,7 +10,7 @@
 
 void main() {
   test('--force cannot be combined with --dry-run', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await runPub(
       args: ['lish', '--force', '--dry-run'],
diff --git a/test/lish/force_does_not_publish_if_there_are_errors_test.dart b/test/lish/force_does_not_publish_if_there_are_errors_test.dart
index c803556..d1cad32 100644
--- a/test/lish/force_does_not_publish_if_there_are_errors_test.dart
+++ b/test/lish/force_does_not_publish_if_there_are_errors_test.dart
@@ -14,7 +14,7 @@
 void main() {
   test('--force does not publish if there are errors', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     // It is an error to publish without a LICENSE file.
     File(d.path(p.join(appPath, 'LICENSE'))).deleteSync();
 
diff --git a/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart b/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
index 882fbf0..4a5ea09 100644
--- a/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
+++ b/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
@@ -15,7 +15,7 @@
 void main() {
   test('--force publishes if there are no warnings or errors', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer, args: ['--force']);
 
diff --git a/test/lish/force_publishes_if_there_are_warnings_test.dart b/test/lish/force_publishes_if_there_are_warnings_test.dart
index cfc4567..124da67 100644
--- a/test/lish/force_publishes_if_there_are_warnings_test.dart
+++ b/test/lish/force_publishes_if_there_are_warnings_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   test('--force publishes if there are warnings', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     var pkg = packageMap(
       'test_pkg',
       '1.0.0',
diff --git a/test/lish/many_files_test.dart b/test/lish/many_files_test.dart
index 7d9940e..50a45f9 100644
--- a/test/lish/many_files_test.dart
+++ b/test/lish/many_files_test.dart
@@ -29,7 +29,7 @@
 
 void main() {
   testWithGolden('displays all files', (context) async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.dir(
       appPath,
       [
@@ -64,7 +64,7 @@
   test(
       'archives and uploads a package with more files than can fit on '
       'the command line', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     int argMax;
     if (Platform.isWindows) {
diff --git a/test/lish/package_creation_provides_a_malformed_error_test.dart b/test/lish/package_creation_provides_a_malformed_error_test.dart
index 491fb6e..662f896 100644
--- a/test/lish/package_creation_provides_a_malformed_error_test.dart
+++ b/test/lish/package_creation_provides_a_malformed_error_test.dart
@@ -14,7 +14,7 @@
 void main() {
   test('package creation provides a malformed error', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/package_creation_provides_a_malformed_success_test.dart b/test/lish/package_creation_provides_a_malformed_success_test.dart
index 3166285..dc3791b 100644
--- a/test/lish/package_creation_provides_a_malformed_success_test.dart
+++ b/test/lish/package_creation_provides_a_malformed_success_test.dart
@@ -14,7 +14,7 @@
 void main() {
   test('package creation provides a malformed success', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/package_creation_provides_an_error_test.dart b/test/lish/package_creation_provides_an_error_test.dart
index e7dbda4..b43670e 100644
--- a/test/lish/package_creation_provides_an_error_test.dart
+++ b/test/lish/package_creation_provides_an_error_test.dart
@@ -14,7 +14,7 @@
 void main() {
   test('package creation provides an error', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/package_creation_provides_invalid_json_test.dart b/test/lish/package_creation_provides_invalid_json_test.dart
index 39c8e45..9a7b935 100644
--- a/test/lish/package_creation_provides_invalid_json_test.dart
+++ b/test/lish/package_creation_provides_invalid_json_test.dart
@@ -12,7 +12,7 @@
 void main() {
   test('package creation provides invalid JSON', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/package_validation_has_a_warning_and_continues_test.dart b/test/lish/package_validation_has_a_warning_and_continues_test.dart
index a5c083b..2e8498c 100644
--- a/test/lish/package_validation_has_a_warning_and_continues_test.dart
+++ b/test/lish/package_validation_has_a_warning_and_continues_test.dart
@@ -17,7 +17,7 @@
 void main() {
   test('package validation has a warning and continues', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     // Publishing without a README.md gives a warning.
     File(d.path(p.join(appPath, 'README.md'))).deleteSync();
 
diff --git a/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart b/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
index f4e4b0b..f44b3a4 100644
--- a/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
+++ b/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
@@ -10,7 +10,7 @@
 
 void main() {
   test('package validation has a warning and is canceled', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     var pkg = packageMap(
       'test_pkg',
       '1.0.0',
diff --git a/test/lish/requires_resolution_before_publishing_test.dart b/test/lish/requires_resolution_before_publishing_test.dart
index 3dfebc0..8edf5cd 100644
--- a/test/lish/requires_resolution_before_publishing_test.dart
+++ b/test/lish/requires_resolution_before_publishing_test.dart
@@ -11,7 +11,7 @@
 void main() {
   test('does not publish if no resolution can be found', () async {
     await servePackages(); // No packages.
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.appDir(dependencies: {'foo': '1.0.0'}).create();
     await runPub(
       args: ['lish'],
diff --git a/test/lish/unicode_file_names_test.dart b/test/lish/unicode_file_names_test.dart
index 4e1ef36..fc60633 100644
--- a/test/lish/unicode_file_names_test.dart
+++ b/test/lish/unicode_file_names_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   test('archives and uploads a package with unicode filenames', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.dir(appPath, [d.file('🦄.yml')]).create();
 
     await servePackages();
diff --git a/test/lish/upload_form_fields_has_a_non_string_value_test.dart b/test/lish/upload_form_fields_has_a_non_string_value_test.dart
index 7793863..5430fb3 100644
--- a/test/lish/upload_form_fields_has_a_non_string_value_test.dart
+++ b/test/lish/upload_form_fields_has_a_non_string_value_test.dart
@@ -13,7 +13,7 @@
 void main() {
   test('upload form fields has a non-string value', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/upload_form_fields_is_not_a_map_test.dart b/test/lish/upload_form_fields_is_not_a_map_test.dart
index d0efb0d..4701d44 100644
--- a/test/lish/upload_form_fields_is_not_a_map_test.dart
+++ b/test/lish/upload_form_fields_is_not_a_map_test.dart
@@ -13,7 +13,7 @@
 void main() {
   test('upload form fields is not a map', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/upload_form_is_missing_fields_test.dart b/test/lish/upload_form_is_missing_fields_test.dart
index fc494ca..8aee08a 100644
--- a/test/lish/upload_form_is_missing_fields_test.dart
+++ b/test/lish/upload_form_is_missing_fields_test.dart
@@ -13,7 +13,7 @@
 void main() {
   test('upload form is missing fields', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/upload_form_is_missing_url_test.dart b/test/lish/upload_form_is_missing_url_test.dart
index e4f0291..508452f 100644
--- a/test/lish/upload_form_is_missing_url_test.dart
+++ b/test/lish/upload_form_is_missing_url_test.dart
@@ -13,7 +13,7 @@
 void main() {
   test('upload form is missing url', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/upload_form_provides_an_error_test.dart b/test/lish/upload_form_provides_an_error_test.dart
index 2eacb24..39051ef 100644
--- a/test/lish/upload_form_provides_an_error_test.dart
+++ b/test/lish/upload_form_provides_an_error_test.dart
@@ -13,7 +13,7 @@
 void main() {
   test('upload form provides an error', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/lish/upload_form_provides_invalid_json_test.dart b/test/lish/upload_form_provides_invalid_json_test.dart
index 14862bd..1b64d5f 100644
--- a/test/lish/upload_form_provides_invalid_json_test.dart
+++ b/test/lish/upload_form_provides_invalid_json_test.dart
@@ -11,7 +11,7 @@
 void main() {
   test('upload form provides invalid JSON', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await servePackages();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
diff --git a/test/lish/upload_form_url_is_not_a_string_test.dart b/test/lish/upload_form_url_is_not_a_string_test.dart
index 45e9aa8..b92e43d 100644
--- a/test/lish/upload_form_url_is_not_a_string_test.dart
+++ b/test/lish/upload_form_url_is_not_a_string_test.dart
@@ -13,7 +13,7 @@
 void main() {
   test('upload form url is not a string', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
 
diff --git a/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart b/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart
index cb5280f..e2192de 100644
--- a/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart
+++ b/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart
@@ -13,7 +13,7 @@
   test(
       'with a malformed credentials.json, authenticates again and '
       'saves credentials.json', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await servePackages();
     await configDir([d.file('pub-credentials.json', '{bad json')]).create();
diff --git a/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart b/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart
index 516204c..860de44 100644
--- a/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart
+++ b/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart
@@ -9,7 +9,7 @@
 
 void main() {
   test('with a pre-existing credentials.json does not authenticate', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await servePackages();
     await d.credentialsFile(globalServer, 'access-token').create();
diff --git a/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart b/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart
index 2237814..419a81e 100644
--- a/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart
+++ b/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart
@@ -17,7 +17,7 @@
   test(
       'with a server-rejected refresh token, authenticates again and '
       'saves credentials.json', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await servePackages();
     await d
diff --git a/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart b/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart
index bca9feb..4f9050c 100644
--- a/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart
+++ b/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart
@@ -14,7 +14,7 @@
   test(
       'with an expired credentials.json, refreshes and saves the '
       'refreshed access token to credentials.json', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await servePackages();
     await d
diff --git a/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart b/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart
index b21b496..a5e495a 100644
--- a/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart
+++ b/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart
@@ -14,7 +14,7 @@
       'with an expired credentials.json without a refresh token, '
       'authenticates again and saves credentials.json', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await d
         .credentialsFile(
diff --git a/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart b/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart
index ba5bc01..6ea2883 100644
--- a/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart
+++ b/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart
@@ -13,7 +13,7 @@
   test(
       'with no credentials.json, authenticates and saves '
       'credentials.json', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await servePackages();
     var pub = await startPublish(globalServer);
     await confirmPublish(pub);
diff --git a/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart b/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart
index c4b8d5b..ecb6ed5 100644
--- a/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart
+++ b/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart
@@ -14,7 +14,7 @@
   test(
       'with server-rejected credentials, authenticates again and saves '
       'credentials.json', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await servePackages();
     await d.credentialsFile(globalServer, 'access-token').create();
     var pub = await startPublish(globalServer);
diff --git a/test/package_list_files_test.dart b/test/package_list_files_test.dart
index a9853e4..773059d 100644
--- a/test/package_list_files_test.dart
+++ b/test/package_list_files_test.dart
@@ -338,7 +338,7 @@
     });
 
     test('.pubignore', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.dir(appPath, [
         d.file('.pubignore', '''
 /lib/ignored.dart
diff --git a/test/token/error_message_test.dart b/test/token/error_message_test.dart
index 5ed36dc..b1b4793 100644
--- a/test/token/error_message_test.dart
+++ b/test/token/error_message_test.dart
@@ -29,7 +29,7 @@
 
 void main() {
   setUp(() async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await servePackages();
     await d.tokensFile({
       'version': 1,
diff --git a/test/token/token_authentication_test.dart b/test/token/token_authentication_test.dart
index 7bd74f1..6ef22c0 100644
--- a/test/token/token_authentication_test.dart
+++ b/test/token/token_authentication_test.dart
@@ -12,7 +12,7 @@
 void main() {
   test('with a pre existing environment token authenticates', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
@@ -33,7 +33,7 @@
 
   test('with a invalid environment token fails with error', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
@@ -54,7 +54,7 @@
 
   test('with a pre existing invalid opaque token fails with error', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
@@ -80,7 +80,7 @@
 
   test('with a pre existing opaque token authenticates', () async {
     await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
diff --git a/test/token/when_receives_401_removes_token_test.dart b/test/token/when_receives_401_removes_token_test.dart
index 9e4d187..cc6560f 100644
--- a/test/token/when_receives_401_removes_token_test.dart
+++ b/test/token/when_receives_401_removes_token_test.dart
@@ -11,7 +11,7 @@
 void main() {
   test('when receives 401 response removes saved token', () async {
     final server = await servePackages();
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.tokensFile({
       'version': 1,
       'hosted': [
diff --git a/test/token/when_receives_403_persists_saved_token_test.dart b/test/token/when_receives_403_persists_saved_token_test.dart
index 2141173..cfcddb7 100644
--- a/test/token/when_receives_403_persists_saved_token_test.dart
+++ b/test/token/when_receives_403_persists_saved_token_test.dart
@@ -10,7 +10,7 @@
 
 void main() {
   test('when receives 403 response persists saved token', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     final server = await servePackages();
     await d.tokensFile({
       'version': 1,
diff --git a/test/validator/changelog_test.dart b/test/validator/changelog_test.dart
index bf274d2..2c0de45 100644
--- a/test/validator/changelog_test.dart
+++ b/test/validator/changelog_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('has a CHANGELOG that includes the current package version', () async {
       await d.dir(appPath, [
diff --git a/test/validator/compiled_dartdoc_test.dart b/test/validator/compiled_dartdoc_test.dart
index 1958d91..75de195 100644
--- a/test/validator/compiled_dartdoc_test.dart
+++ b/test/validator/compiled_dartdoc_test.dart
@@ -13,7 +13,7 @@
 Validator compiledDartdoc() => CompiledDartdocValidator();
 
 void main() {
-  setUp(d.validPackage.create);
+  setUp(d.validPackage().create);
 
   group('should consider a package valid if it', () {
     test('looks normal', () => expectValidationDeprecated(compiledDartdoc));
diff --git a/test/validator/dependency_override_test.dart b/test/validator/dependency_override_test.dart
index 7145b3f..308140a 100644
--- a/test/validator/dependency_override_test.dart
+++ b/test/validator/dependency_override_test.dart
@@ -14,7 +14,7 @@
       'overrides', () async {
     final server = await servePackages();
     server.serve('foo', '3.0.0');
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await d.dir(appPath, [
       d.validPubspec(
@@ -32,7 +32,7 @@
     test('it has only non-dev dependency overrides', () async {
       final server = await servePackages();
       server.serve('foo', '3.0.0');
-      await d.validPackage.create();
+      await d.validPackage().create();
 
       await d.dir(appPath, [
         d.validPubspec(
@@ -50,7 +50,7 @@
     test('it has a pubspec_overrides.yaml', () async {
       final server = await servePackages();
       server.serve('foo', '3.0.0');
-      await d.validPackage.create();
+      await d.validPackage().create();
 
       await d.dir(appPath, [
         d.validPubspec(
@@ -73,7 +73,7 @@
       server.serve('foo', '3.0.0');
       server.serve('bar', '3.0.0');
 
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.dir(appPath, [
         d.validPubspec(
           extras: {
diff --git a/test/validator/deprecated_fields_test.dart b/test/validator/deprecated_fields_test.dart
index 0206701..7ff752a 100644
--- a/test/validator/deprecated_fields_test.dart
+++ b/test/validator/deprecated_fields_test.dart
@@ -13,7 +13,7 @@
 Validator deprecatedFields() => DeprecatedFieldsValidator();
 
 void main() {
-  setUp(d.validPackage.create);
+  setUp(d.validPackage().create);
 
   test(
     'should not warn if neither transformers or web is included',
diff --git a/test/validator/directory_test.dart b/test/validator/directory_test.dart
index d1eedf8..c8362c8 100644
--- a/test/validator/directory_test.dart
+++ b/test/validator/directory_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('looks normal', () => expectValidationDeprecated(directory));
 
@@ -41,7 +41,7 @@
   group(
       'should consider a package invalid if it has a top-level directory '
       'named', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     var names = [
       'benchmarks',
diff --git a/test/validator/executable_test.dart b/test/validator/executable_test.dart
index 497c16b..76e69c6 100644
--- a/test/validator/executable_test.dart
+++ b/test/validator/executable_test.dart
@@ -13,7 +13,7 @@
 Validator executable() => ExecutableValidator();
 
 void main() {
-  setUp(d.validPackage.create);
+  setUp(d.validPackage().create);
 
   group('should consider a package valid if it', () {
     test('has executables that are present', () async {
diff --git a/test/validator/file_case_test.dart b/test/validator/file_case_test.dart
index 67d8200..4142000 100644
--- a/test/validator/file_case_test.dart
+++ b/test/validator/file_case_test.dart
@@ -27,7 +27,7 @@
 
 void main() {
   test('Recognizes files that only differ in capitalization.', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
     await d.dir(appPath, [d.file('Pubspec.yaml')]).create();
     await expectValidation(
       allOf(
diff --git a/test/validator/flutter_constraint_test.dart b/test/validator/flutter_constraint_test.dart
index 815e6e4..d3a9eaa 100644
--- a/test/validator/flutter_constraint_test.dart
+++ b/test/validator/flutter_constraint_test.dart
@@ -26,7 +26,7 @@
 }) async {
   fakeFlutterRoot = d.dir('fake_flutter_root', [d.file('version', '1.23.0')]);
   await fakeFlutterRoot.create();
-  await d.validPackage.create();
+  await d.validPackage().create();
   await d.dir(appPath, [
     d.pubspec({
       'name': 'test_pkg',
diff --git a/test/validator/flutter_plugin_format_test.dart b/test/validator/flutter_plugin_format_test.dart
index de06aa3..0a5bd4c 100644
--- a/test/validator/flutter_plugin_format_test.dart
+++ b/test/validator/flutter_plugin_format_test.dart
@@ -15,7 +15,7 @@
 void main() {
   group('should consider a package valid if it', () {
     test('is not a plugin', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       return expectValidationDeprecated(flutterPluginFormat);
     });
 
diff --git a/test/validator/gitignore_test.dart b/test/validator/gitignore_test.dart
index abd12bd..cf3e4d5 100644
--- a/test/validator/gitignore_test.dart
+++ b/test/validator/gitignore_test.dart
@@ -31,7 +31,7 @@
       'should consider a package valid if it contains no checked in otherwise ignored files',
       () async {
     await d.git('myapp', [
-      ...d.validPackage.contents,
+      ...d.validPackage().contents,
       d.file('foo.txt'),
     ]).create();
 
@@ -55,7 +55,7 @@
 
   test('should not fail on missing git', () async {
     await d.git('myapp', [
-      ...d.validPackage.contents,
+      ...d.validPackage().contents,
       d.file('.gitignore', '*.txt'),
       d.file('foo.txt'),
     ]).create();
@@ -75,7 +75,7 @@
         'myapp',
         [
           d.file('foo.txt'),
-          ...d.validPackage.contents,
+          ...d.validPackage().contents,
         ],
       ),
     ]).create();
@@ -107,7 +107,7 @@
 
   test('Should not follow symlinks', () async {
     await d.git('myapp', [
-      ...d.validPackage.contents,
+      ...d.validPackage().contents,
     ]).create();
     final packageRoot = p.join(d.sandbox, 'myapp');
     await pubGet(workingDirectory: packageRoot);
diff --git a/test/validator/language_version_test.dart b/test/validator/language_version_test.dart
index 9832fd2..5a198ae 100644
--- a/test/validator/language_version_test.dart
+++ b/test/validator/language_version_test.dart
@@ -12,7 +12,7 @@
   required String sdkConstraint,
   String? libraryLanguageVersion,
 }) async {
-  await d.validPackage.create();
+  await d.validPackage().create();
   await d.dir(appPath, [
     d.validPubspec(
       extras: {
diff --git a/test/validator/leak_detection_test.dart b/test/validator/leak_detection_test.dart
index a890658..cf8876f 100644
--- a/test/validator/leak_detection_test.dart
+++ b/test/validator/leak_detection_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('contains a source file without secrets', () async {
       await d.dir(appPath, [
diff --git a/test/validator/license_test.dart b/test/validator/license_test.dart
index 73ab202..b500777 100644
--- a/test/validator/license_test.dart
+++ b/test/validator/license_test.dart
@@ -17,12 +17,12 @@
 void main() {
   group('should consider a package valid if it', () {
     test('looks normal', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await expectValidationDeprecated(license);
     });
 
     test('has both LICENSE and UNLICENSE file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.file(path.join(appPath, 'UNLICENSE'), '').create();
       await expectValidationDeprecated(license);
     });
@@ -30,28 +30,28 @@
 
   group('should warn if it', () {
     test('has only a COPYING file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'COPYING'), '').create();
       await expectValidationDeprecated(license, warnings: isNotEmpty);
     });
 
     test('has only an UNLICENSE file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'UNLICENSE'), '').create();
       await expectValidationDeprecated(license, warnings: isNotEmpty);
     });
 
     test('has only a prefixed LICENSE file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'MIT_LICENSE'), '').create();
       await expectValidationDeprecated(license, warnings: isNotEmpty);
     });
 
     test('has only a suffixed LICENSE file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'LICENSE.md'), '').create();
       await expectValidationDeprecated(license, warnings: isNotEmpty);
@@ -60,13 +60,13 @@
 
   group('should consider a package invalid if it', () {
     test('has no LICENSE file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await expectValidationDeprecated(license, errors: isNotEmpty);
     });
 
     test('has a prefixed UNLICENSE file', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'MIT_UNLICENSE'), '').create();
       await expectValidationDeprecated(license, errors: isNotEmpty);
@@ -74,7 +74,7 @@
 
     test('has a .gitignored LICENSE file', () async {
       var repo = d.git(appPath, [d.file('.gitignore', 'LICENSE')]);
-      await d.validPackage.create();
+      await d.validPackage().create();
       await repo.create();
       await expectValidationDeprecated(license, errors: isNotEmpty);
     });
diff --git a/test/validator/name_test.dart b/test/validator/name_test.dart
index 267cdd6..667947a 100644
--- a/test/validator/name_test.dart
+++ b/test/validator/name_test.dart
@@ -16,7 +16,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('looks normal', () => expectValidationDeprecated(name));
 
@@ -41,7 +41,7 @@
   });
 
   group('should consider a package invalid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('has a package name that contains upper-case letters', () async {
       await d.dir(appPath, [d.libPubspec('TestPkg', '1.0.0')]).create();
diff --git a/test/validator/pubspec_field_test.dart b/test/validator/pubspec_field_test.dart
index 1637115..694cb47 100644
--- a/test/validator/pubspec_field_test.dart
+++ b/test/validator/pubspec_field_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('looks normal', () => expectValidationDeprecated(pubspecField));
 
@@ -75,7 +75,7 @@
   });
 
   group('should consider a package invalid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('is missing the "description" field', () async {
       var pkg = packageMap('test_pkg', '1.0.0');
diff --git a/test/validator/pubspec_test.dart b/test/validator/pubspec_test.dart
index fd1a255..995f376 100644
--- a/test/validator/pubspec_test.dart
+++ b/test/validator/pubspec_test.dart
@@ -11,7 +11,7 @@
 
 void main() {
   test('should consider a package valid if it has a pubspec', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await expectValidationDeprecated(PubspecValidator.new);
   });
@@ -19,7 +19,7 @@
   test('should consider a package invalid if it has a .gitignored pubspec',
       () async {
     var repo = d.git(appPath, [d.file('.gitignore', 'pubspec.yaml')]);
-    await d.validPackage.create();
+    await d.validPackage().create();
     await repo.create();
 
     await expectValidationDeprecated(PubspecValidator.new, errors: isNotEmpty);
diff --git a/test/validator/pubspec_typo_test.dart b/test/validator/pubspec_typo_test.dart
index bc784d4..8af133e 100644
--- a/test/validator/pubspec_typo_test.dart
+++ b/test/validator/pubspec_typo_test.dart
@@ -14,7 +14,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('looks normal', () => expectValidationDeprecated(pubspecTypo));
 
@@ -57,7 +57,7 @@
   });
 
   group('should has warnings if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('contains typos', () async {
       await d.dir(appPath, [
diff --git a/test/validator/readme_test.dart b/test/validator/readme_test.dart
index 67fb79e..02a29d9 100644
--- a/test/validator/readme_test.dart
+++ b/test/validator/readme_test.dart
@@ -17,12 +17,12 @@
 void main() {
   group('should consider a package valid if it', () {
     test('looks normal', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await expectValidationDeprecated(readme);
     });
 
     test('has a non-primary readme with invalid utf-8', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.dir(appPath, [
         d.file('README.x.y.z', [192])
       ]).create();
@@ -30,7 +30,7 @@
     });
 
     test('has a gitignored README with invalid utf-8', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       var repo = d.git(appPath, [
         d.file('README', [192]),
         d.file('.gitignore', 'README')
@@ -42,20 +42,20 @@
 
   group('should consider a package invalid if it', () {
     test('has no README', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
 
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await expectValidationDeprecated(readme, warnings: isNotEmpty);
     });
 
     test('has only a .gitignored README', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.git(appPath, [d.file('.gitignore', 'README.md')]).create();
       await expectValidationDeprecated(readme, warnings: isNotEmpty);
     });
 
     test('has a primary README with invalid utf-8', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.dir(appPath, [
         d.file('README', [192])
       ]).create();
@@ -63,21 +63,21 @@
     });
 
     test('has only a non-primary readme', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await d.dir(appPath, [d.file('README.whatever')]).create();
       await expectValidationDeprecated(readme, warnings: isNotEmpty);
     });
 
     test('Uses only deprecated readme name .markdown', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await d.dir(appPath, [d.file('README.markdown')]).create();
       await expectValidationDeprecated(readme, warnings: isNotEmpty);
     });
 
     test('Uses only deprecated readme name .mdown', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await d.dir(appPath, [d.file('README.mdown')]).create();
       await expectValidationDeprecated(readme, warnings: isNotEmpty);
diff --git a/test/validator/relative_version_numbering_test.dart b/test/validator/relative_version_numbering_test.dart
index 6b73dff..a1638b3 100644
--- a/test/validator/relative_version_numbering_test.dart
+++ b/test/validator/relative_version_numbering_test.dart
@@ -13,7 +13,7 @@
 Validator validator() => RelativeVersionNumberingValidator();
 
 Future<void> setup({required String sdkConstraint}) async {
-  await d.validPackage.create();
+  await d.validPackage().create();
   await d.dir(appPath, [
     d.pubspec({
       'name': 'test_pkg',
@@ -26,6 +26,123 @@
 }
 
 void main() {
+  test('Hints about not publishing latest', () async {
+    final server = await servePackages();
+    server.serve(
+      'test_pkg',
+      '2.0.2',
+    );
+    await d.validPackage().create();
+
+    await expectValidationHint('''
+The latest published version is 2.0.2.
+  Your version 1.0.0 is earlier than that.
+''');
+  });
+
+  test('Hints incrementing more than needed', () async {
+    final server = await servePackages();
+    server.serve(
+      'test_pkg',
+      '1.0.2',
+    );
+
+    const notIncrementalHintText = '''
+* The previous version is 1.0.2.
+  
+  It seems you are not publishing an incremental update.
+  
+  Consider one of:
+  * 2.0.0 for a breaking release.
+  * 1.1.0 for a minor release.
+  * 1.0.3 for a patch release.
+''';
+
+    await d.validPackage(version: '1.0.4').create();
+    await expectValidationHint(notIncrementalHintText);
+    await d.validPackage(version: '1.3.0').create();
+    await expectValidationHint(notIncrementalHintText);
+    await d.validPackage(version: '1.1.1').create();
+    await expectValidationHint(notIncrementalHintText);
+  });
+
+  test('Hints incrementing more than needed after a prerelease', () async {
+    final server = await servePackages();
+    server.serve(
+      'test_pkg',
+      '1.0.2-pre',
+    );
+
+    const notIncrementalHintText = '''
+* The previous version is 1.0.2-pre.
+  
+  It seems you are not publishing an incremental update.
+  
+  Consider one of:
+  * 2.0.0 for a breaking release.
+  * 1.1.0 for a minor release.
+  * 1.0.2 for a patch release.
+''';
+
+    await d.validPackage(version: '1.0.4').create();
+    await expectValidationHint(notIncrementalHintText);
+    await d.validPackage(version: '1.3.0').create();
+    await expectValidationHint(notIncrementalHintText);
+    await d.validPackage(version: '1.1.1').create();
+    await expectValidationHint(notIncrementalHintText);
+  });
+
+  test('Hints incrementing more than after pre 1.0', () async {
+    final server = await servePackages();
+    server.serve(
+      'test_pkg',
+      '0.0.1',
+    );
+
+    const notIncrementalHintText = '''
+* The previous version is 0.0.1.
+  
+  It seems you are not publishing an incremental update.
+  
+  Consider one of:
+  * 1.0.0 for a first major release.
+  * 0.1.0 for a breaking release.
+  * 0.0.2 for a minor release.
+''';
+
+    await d.validPackage(version: '0.0.3').create();
+    await expectValidationHint(notIncrementalHintText);
+    await d.validPackage(version: '0.1.1').create();
+    await expectValidationHint(notIncrementalHintText);
+    await d.validPackage(version: '1.0.1').create();
+    await expectValidationHint(notIncrementalHintText);
+  });
+
+  test('Releasing a prerelease of incremental version causes no hint',
+      () async {
+    final server = await servePackages();
+    server.serve(
+      'test_pkg',
+      '1.0.0',
+    );
+    await d.validPackage(version: '1.0.1-dev').create();
+    await expectValidation();
+    await d.validPackage(version: '1.1.0-dev').create();
+    await expectValidation();
+    await d.validPackage(version: '2.0.0-dev').create();
+    await expectValidation();
+  });
+
+  test('Releasing the prereleased version causes no hint', () async {
+    final server = await servePackages();
+    server.serve(
+      'test_pkg',
+      '1.0.0-dev',
+    );
+    await d.validPackage().create();
+    await expectValidation();
+  });
+
   group('should consider a package valid if it', () {
     test('is opting in to null-safety with previous null-safe version',
         () async {
@@ -78,7 +195,15 @@
         );
 
       await setup(sdkConstraint: '>=2.12.0 <3.0.0');
-      await expectValidationDeprecated(validator);
+      await expectValidationDeprecated(
+        validator,
+        hints: [
+          // Nothing about null-safety
+          '''
+The latest published version is 2.0.1.
+Your version 1.0.0 is earlier than that.'''
+        ],
+      );
     });
 
     test('is opting in to null-safety with no existing versions', () async {
@@ -124,7 +249,15 @@
       );
 
       await setup(sdkConstraint: '>=2.12.0 <3.0.0');
-      await expectValidationDeprecated(validator, hints: isNotEmpty);
+      await expectValidationDeprecated(
+        validator,
+        hints: [
+          '''
+You're about to publish a package that opts into null safety.
+The previous version (0.0.1) isn't opted in.
+See https://dart.dev/null-safety/migration-guide for best practices.'''
+        ],
+      );
     });
 
     test(
@@ -147,7 +280,18 @@
         );
 
       await setup(sdkConstraint: '>=2.12.0 <3.0.0');
-      await expectValidationDeprecated(validator, hints: isNotEmpty);
+      await expectValidationDeprecated(
+        validator,
+        hints: [
+          '''
+The latest published version is 2.0.0.
+Your version 1.0.0 is earlier than that.''',
+          '''
+You're about to publish a package that opts into null safety.
+The previous version (0.0.1) isn't opted in.
+See https://dart.dev/null-safety/migration-guide for best practices.'''
+        ],
+      );
     });
 
     test(
@@ -170,7 +314,15 @@
         );
 
       await setup(sdkConstraint: '>=2.12.0 <3.0.0');
-      await expectValidationDeprecated(validator, hints: isNotEmpty);
+      await expectValidationDeprecated(
+        validator,
+        hints: [
+          '''
+You're about to publish a package that opts into null safety.
+The previous version (0.0.2-dev) isn't opted in.
+See https://dart.dev/null-safety/migration-guide for best practices.'''
+        ],
+      );
     });
 
     test(
@@ -185,7 +337,15 @@
           'environment': {'sdk': '>=2.9.0<3.0.0'}
         },
       );
-      await expectValidationDeprecated(validator, hints: isNotEmpty);
+      await expectValidationDeprecated(
+        validator,
+        hints: [
+          '''
+You're about to publish a package that opts into null safety.
+The previous version (0.0.2-dev) isn't opted in.
+See https://dart.dev/null-safety/migration-guide for best practices.'''
+        ],
+      );
     });
   });
 }
diff --git a/test/validator/sdk_constraint_test.dart b/test/validator/sdk_constraint_test.dart
index b050c72..ff2cdee 100644
--- a/test/validator/sdk_constraint_test.dart
+++ b/test/validator/sdk_constraint_test.dart
@@ -15,7 +15,7 @@
 void main() {
   group('should consider a package valid if it', () {
     test('has no SDK constraint', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await expectValidationDeprecated(sdkConstraint);
     });
 
diff --git a/test/validator/size_test.dart b/test/validator/size_test.dart
index 4f179c8..02a093c 100644
--- a/test/validator/size_test.dart
+++ b/test/validator/size_test.dart
@@ -21,7 +21,7 @@
 
 void main() {
   test('considers a package valid if it is <= 100 MB', () async {
-    await d.validPackage.create();
+    await d.validPackage().create();
 
     await expectValidationDeprecated(SizeValidator.new, size: 100);
     await expectValidationDeprecated(SizeValidator.new, size: 100 * (1 << 20));
@@ -30,7 +30,7 @@
   group('considers a package invalid if it is more than 100 MB', () {
     test('package is not under source control and no .gitignore exists',
         () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
 
       await expectSizeValidationError(
         equals('Your package is 100.0 MB. Hosted packages must '
@@ -39,7 +39,7 @@
     });
 
     test('package is not under source control and .gitignore exists', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.dir(appPath, [d.file('.gitignore', 'ignored')]).create();
 
       await expectSizeValidationError(
@@ -52,7 +52,7 @@
     });
 
     test('package is under source control and no .gitignore exists', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.git(appPath).create();
 
       await expectSizeValidationError(
@@ -65,7 +65,7 @@
     });
 
     test('package is under source control and .gitignore exists', () async {
-      await d.validPackage.create();
+      await d.validPackage().create();
       await d.git(appPath, [d.file('.gitignore', 'ignored')]).create();
 
       await expectSizeValidationError(
diff --git a/test/validator/strict_dependencies_test.dart b/test/validator/strict_dependencies_test.dart
index 26e8d7e..798cad1 100644
--- a/test/validator/strict_dependencies_test.dart
+++ b/test/validator/strict_dependencies_test.dart
@@ -15,7 +15,7 @@
 
 void main() {
   group('should consider a package valid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('looks normal', () => expectValidationDeprecated(strictDeps));
 
@@ -237,7 +237,7 @@
   });
 
   group('should consider a package invalid if it', () {
-    setUp(d.validPackage.create);
+    setUp(d.validPackage().create);
 
     test('has an invalid String value', () async {
       await d.file(path.join(appPath, 'lib', 'library.dart'), r'''