From 817c4e5195bcc1af727f99c8a87996ab895f2ab9 Mon Sep 17 00:00:00 2001 From: Jason McIntosh Date: Sun, 21 Jun 2026 20:04:17 -0400 Subject: [PATCH 1/2] Collapse _build_posts double sort into one comparator (#50) _build_posts pre-sorted source files by basename and then re-sorted the constructed posts by date, relying on sort stability to keep the basename order as a tiebreak. Replace the two passes with a single sort whose comparator is date-descending, basename-ascending -- the same rule _ordered_basenames uses on the incremental path, so full and incremental publishes now agree on ordering. The grep still accepts .md and .markdown. Output order is unchanged; adds t/post_sort.t to lock the contract, including the mixed-extension, same-date tiebreak case. Co-Authored-By: Claude Opus 4.8 --- lib/Plerd.pm | 18 ++++++++++----- t/post_sort.t | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 t/post_sort.t diff --git a/lib/Plerd.pm b/lib/Plerd.pm index eba0ec5..b57141d 100644 --- a/lib/Plerd.pm +++ b/lib/Plerd.pm @@ -829,11 +829,19 @@ sub _build_recent_posts { sub _build_posts { my $self = shift; - my @posts = sort { $b->date <=> $a->date } - map { Plerd::Post->new( plerd => $self, source_file => $_ ) } - sort { $a->basename cmp $b->basename } - grep { /\.markdown$|\.md$/ } - $self->source_directory->children + # Newest publication date first, ties broken by source basename. This is a + # single sort (rather than a basename pre-sort feeding a date sort) and uses + # the same comparator as _ordered_basenames, so a full publish and an + # incremental publish agree on order. The grep keeps both .md and .markdown + # sources. + my @posts = + sort { + $b->date <=> $a->date + || $a->source_file->basename cmp $b->source_file->basename + } + map { Plerd::Post->new( plerd => $self, source_file => $_ ) } + grep { /\.markdown$|\.md$/ } + $self->source_directory->children ; return \@posts; diff --git a/t/post_sort.t b/t/post_sort.t new file mode 100644 index 0000000..d2add96 --- /dev/null +++ b/t/post_sort.t @@ -0,0 +1,62 @@ +use warnings; +use strict; +use Test::More; +use Path::Class::Dir; +use Path::Class::File; +use URI; + +use FindBin; +use lib "$FindBin::Bin/../lib"; + +use_ok( 'Plerd' ); +use Plerd::Init; + +# Regression test for GitHub issue #50: the post list must come out in a +# deterministic order -- newest publication date first, ties broken by source +# basename -- even when the source directory mixes .md and .markdown files. +# This matches the ordering the incremental publisher derives from the post +# index (see Plerd::_ordered_basenames), so both code paths agree. + +my $blog_dir = Path::Class::Dir->new( "$FindBin::Bin/post_sort_blog" ); +$blog_dir->rmtree; +Plerd::Init::initialize( $blog_dir->stringify, 0 ); + +my $source_dir = Path::Class::Dir->new( $blog_dir, 'source' ); + +# Two posts share a date (forcing the basename tiebreak) and use different +# extensions; a third is older. Explicit timestamps keep dates exact. +my %sources = ( + '2020-06-20-bravo.md' => '2020-06-20T10:00:00', + '2020-06-20-alpha.markdown' => '2020-06-20T10:00:00', + '2019-01-01-charlie.md' => '2019-01-01T10:00:00', +); +for my $basename ( keys %sources ) { + Path::Class::File->new( $source_dir, $basename )->spew( + iomode => '>:encoding(utf8)', + "title: $basename\ntime: $sources{ $basename }\n\nBody.\n", + ); +} + +my $plerd = Plerd->new( + path => $blog_dir->stringify, + title => 'Test Blog', + author_name => 'Nobody', + author_email => 'nobody@example.com', + base_uri => URI->new( 'http://blog.example.com/' ), +); + +my @order = map { $_->source_file->basename } @{ $plerd->posts }; + +is_deeply( + \@order, + [ + '2020-06-20-alpha.markdown', # same date as bravo, basename sorts first + '2020-06-20-bravo.md', + '2019-01-01-charlie.md', # oldest, last + ], + 'Posts sort newest-first with a stable basename tiebreak across extensions.' +); + +$blog_dir->rmtree; + +done_testing(); From bc5c2ac2e5a00140c690f08bcc74886d28b312a0 Mon Sep 17 00:00:00 2001 From: Jason McIntosh Date: Mon, 22 Jun 2026 10:44:43 -0400 Subject: [PATCH 2/2] Restore deterministic post-construction order (#50) CI caught that the previous commit broke t/basic.t on Linux: removing the basename sort that ran *before* the map meant posts were constructed in readdir order. Construction has side effects (it can die on a malformed file, rewrites source files, resolves tag-case conflicts first-seen-wins), so a non-deterministic order made publish_all report no-title.md's error before bad-date.md's on ext4, failing the "not in W3C format" assertion. Restore the basename pre-sort so construction is deterministic, keeping the explicit date-then-basename comparator on the built posts. The pre-sort orders file objects (not posts), so it's cheap -- it isn't redundant, it's load-bearing for determinism. Co-Authored-By: Claude Opus 4.8 --- lib/Plerd.pm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Plerd.pm b/lib/Plerd.pm index b57141d..1f0739e 100644 --- a/lib/Plerd.pm +++ b/lib/Plerd.pm @@ -829,17 +829,17 @@ sub _build_recent_posts { sub _build_posts { my $self = shift; - # Newest publication date first, ties broken by source basename. This is a - # single sort (rather than a basename pre-sort feeding a date sort) and uses - # the same comparator as _ordered_basenames, so a full publish and an - # incremental publish agree on order. The grep keeps both .md and .markdown - # sources. + # Newest publication date first, ties broken by source basename, using the + # same comparator as _ordered_basenames so a full publish and an incremental + # publish agree on order. The inner sort guarantees order regardless of + # readdir order, and the grep keeps both .md and .markdown sources. my @posts = sort { $b->date <=> $a->date || $a->source_file->basename cmp $b->source_file->basename } map { Plerd::Post->new( plerd => $self, source_file => $_ ) } + sort { $a->basename cmp $b->basename } grep { /\.markdown$|\.md$/ } $self->source_directory->children ;