@@ -14,6 +14,7 @@ import 'dart:io';
1414
1515const _baseTag = 'deployed' ;
1616const _redirectsFile = 'redirects.json' ;
17+ const _removedFile = 'removed.json' ;
1718
1819Future <void > main (List <String > args) async {
1920 final mode = args.isEmpty ? '' : args.first;
@@ -58,6 +59,18 @@ Future<Map<String, String>> _loadRedirects() async {
5859 return decoded.map ((k, v) => MapEntry (k, v as String ));
5960}
6061
62+ /// URLs that intentionally stopped existing with no replacement — e.g. a
63+ /// page that should never have been published. Listed explicitly so a
64+ /// missing redirect is a recorded decision, not a silently-skipped check.
65+ Future <Set <String >> _loadRemoved () async {
66+ final file = File (_removedFile);
67+ if (! await file.exists ()) return {};
68+ final content = await file.readAsString ();
69+ if (content.trim ().isEmpty) return {};
70+ final decoded = jsonDecode (content) as List <dynamic >;
71+ return decoded.map ((e) => e as String ).toSet ();
72+ }
73+
6174Future <void > _saveRedirects (Map <String , String > redirects) async {
6275 final sortedKeys = redirects.keys.toList ()..sort ();
6376 final sorted = {for (final k in sortedKeys) k: redirects[k]! };
@@ -110,6 +123,7 @@ Future<void> _runCheck() async {
110123
111124 var redirects = await _loadRedirects ();
112125 final originalRedirects = Map <String , String >.from (redirects);
126+ final removed = await _loadRemoved ();
113127 final renames = < String , String > {};
114128 final deletions = < String > {};
115129
@@ -133,9 +147,13 @@ Future<void> _runCheck() async {
133147 redirects = _flatten (redirects);
134148
135149 // A deletion is fine if it's covered by a redirect (renamed/merged into
136- // something that still resolves). Otherwise it's a URL that would start
137- // 404ing with no replacement, which needs a human decision.
138- final uncovered = deletions.where ((url) => ! redirects.containsKey (url)).toList ()..sort ();
150+ // something that still resolves) or explicitly marked as intentionally
151+ // removed in removed.json. Otherwise it's a URL that would start 404ing
152+ // with no replacement, which needs a human decision.
153+ final uncovered = deletions
154+ .where ((url) => ! redirects.containsKey (url) && ! removed.contains (url))
155+ .toList ()
156+ ..sort ();
139157
140158 if (renames.isNotEmpty) {
141159 stdout.writeln ('Detected ${renames .length } content move(s):' );
@@ -156,8 +174,9 @@ Future<void> _runCheck() async {
156174 stderr.writeln (' $url ' );
157175 }
158176 stderr.writeln (
159- '\n Either restore the content, or add an entry to redirects.json '
160- 'mapping the old URL to wherever it should now go.' ,
177+ '\n Either restore the content, add an entry to redirects.json mapping '
178+ 'the old URL to wherever it should now go, or add the URL to '
179+ 'removed.json if it was deleted intentionally with no replacement.' ,
161180 );
162181 exit (1 );
163182 }
0 commit comments