App-karr
view release on metacpan or search on metacpan
lib/App/karr/Role/DependencyCheck.pm view on Meta::CPAN
our $VERSION = '0.500';
use Moo::Role;
# What this role calls on its consumer, said out loud (ticket #128). It used to
# declare nothing, and got away with it only because every consumer happened to
# compose the roles that supply these: store from
# App::karr::Role::BoardDiscovery, find_task from App::karr::Role::BoardAccess,
# json from App::karr::Role::Output, quiet from
# App::karr::Role::SyncLifecycle. App::karr::Role::TaskMutation composes this
# role, so the next command to reach for the mutation path would have inherited
# methods whose collaborators nobody had checked for -- and found out at the
# moment a warning was due, as a "Can't locate object method", rather than at
# compile time.
#
# json is on the list since ticket #137 split the set-time helpers off into
# App::karr::Role::DependencyArgs. It could not be until then: this role also
# carried parse_dependency_ids and assert_dependencies_exist, and the command
# that composed it for those two alone -- create -- has no --json, so requiring
# json refused a consumer that never reaches the reporting half. Leaving it out
# was the narrower hole rather than none: `$self->json || $self->quiet`
# evaluates json first and unconditionally, so a consumer missing it broke on
# every warning, not only the ones without --json.
requires qw( store find_task json quiet );
# Keyed by task id rather than a flat list, because check_dependencies runs
# inside a compare-and-swap callback that re-runs when another agent gets in
# first (App::karr::Role::TaskMutation/update_task_guarded,
# App::karr::Cmd::Pick/_claim_under_lock). A list would grow one copy of every
# warning per attempt; a keyed slot is replaced by the attempt that wins.
has _dependency_warnings => (
is => 'ro',
default => sub { {} },
);
sub check_dependencies {
my ( $self, $task, $new_status ) = @_;
my $id = $task->id;
delete $self->_dependency_warnings->{$id};
# A move into a terminal status is not taking work up, it is finishing it,
# and what a finished card was once waiting for is no longer anybody's
# decision to make. The board's own statuses answer this, not done/archived
# (tickets #67, #98).
return () if $self->store->is_terminal_status($new_status);
my @deps = @{ $task->depends_on };
return () unless @deps;
my @warnings;
for my $dep_id (@deps) {
my $dep = $self->find_task($dep_id);
# A deliberate divergence from the reference. kanban-md treats an id
# that is not on the board as *satisfied*
# (internal/board/filter.go:151-154): "Missing dependency IDs can occur
# after legacy hard-deletes. Treat as satisfied so dependents are
# recoverable via edit/cleanup." That reasoning is about not stranding a
# card, and it is sound there, where an unsatisfied dependency makes the
# card unpickable. Here nothing is blocked, so there is no card to
# strand -- and a dependency pointing at an id that does not exist is
# exactly the kind of thing whoever is about to start work wants told.
if ( !$dep ) {
push @warnings, sprintf
'Warning: task %s depends on task %s, which does not exist on this board',
$id, $dep_id;
next;
}
next if $self->store->is_terminal_status( $dep->status );
push @warnings, sprintf
'Warning: task %s depends on task %s, which is still %s',
$id, $dep_id, $dep->status;
}
$self->_dependency_warnings->{$id} = \@warnings if @warnings;
return @warnings;
}
sub dependency_report {
my ( $self, $id ) = @_;
my $warnings = $self->_dependency_warnings->{$id};
return () unless $warnings && @$warnings;
print STDERR map { "$_\n" } @$warnings
unless $self->json || $self->quiet;
return ( dependency_warnings => $warnings );
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
App::karr::Role::DependencyCheck - Warn when a card is taken up while its dependencies are unfinished
=head1 VERSION
version 0.500
=head1 DESCRIPTION
C<depends_on> was stored, round-tripped and written into the frontmatter by
L<App::karr::Task> long before anything read it. That is worse than a missing
feature: a card recording C<< depends_on: [5] >> looked as though karr would
hold it back until 5 was finished -- the field was accepted, kept and
materialized -- while C<move>, C<edit --status> and C<pick> handed it out with
no word said (ticket #123).
This role is what reads it. Taking a card up with unsatisfied dependencies
B<proceeds and exits 0>, but says so. "Taking up" is a status change into a
( run in 1.183 second using v1.01-cache-2.11-cpan-788537b7465 )