App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/Cmd/Delete.pm  view on Meta::CPAN

    my ($id) = @_;

    my $task = $self->find_task($id);
    # The unguarded pre-read fires before delete_task_guarded below, so this is
    # the not-found a caller normally meets; the guard raises the same line only
    # on the race where the card vanishes in the window. One spelling for both,
    # and for every other command on the mutation path (ticket k264).
    die $self->task_not_found($id) unless $task;

    # A live claim blocks the delete whoever holds it -- an empty claimant, the
    # way kanban-md's cmd/delete.go calls CheckClaim. Neither implementation
    # gives delete a --claim option, so releasing the claim (or letting it
    # expire) is the way through, for the holder as much as for anybody else.
    $self->check_claim($task, undef);

    # Before the confirmation, never after it. The point of the warning is that
    # it can still change the answer, and the operator who reads it and types
    # "n" is the one it worked on -- so it deliberately does not wait for the
    # write the way App::karr::Role::DependencyCheck/dependency_report waits for
    # its own. That rule ("a warning about a move that then lost its
    # compare-and-swap is a warning about something that did not happen") is
    # about a card that survives to be read afterwards. Here the write is what
    # removes the card, so a warning that waits for it is a warning about
    # something nobody can choose to keep any more.
    #
    # Under --yes there is no prompt to precede and it comes anyway: --yes is
    # the mode agents delete in, so a warning only on the interactive path warns
    # exactly where nobody is left to read it.
    my @dependents = $self->_dependent_warnings($task);
    # And what this card says about the boards it cannot see (#242): the far end
    # of an escalation is a tag on the card being deleted, so naming it costs
    # nothing remote.
    my @cross = $self->_cross_board_warnings($task);
    # The channel App::karr::Role::DependencyCheck argues for one module over,
    # rather than a third convention: the human copy on STDERR so STDOUT stays
    # parseable, --quiet silencing that copy, and --json carrying the identical
    # sentence in the result object below because a JSON consumer never reads
    # STDERR.
    print STDERR map { "$_\n" } @dependents, @cross
      unless $self->json || $self->quiet;
    my @warning_report = (
      ( @dependents ? ( dependent_warnings   => \@dependents ) : () ),
      ( @cross      ? ( cross_board_warnings => \@cross )      : () ),
    );

    unless ($self->yes) {
      # STDERR, and not only under --json (#248). The question used to go to
      # STDOUT, which put a bare `Delete task 1: A? [y/N] ` in front of the
      # result object: the object was there, but the stream as a whole would
      # not decode, and this is the command whose output a caller is most
      # likely to read before doing something irreversible.
      #
      # The channel is unconditional rather than a branch on --json, for three
      # reasons. A prompt is not a result -- `deleted` below is the result, and
      # the question is dialogue, which is what STDERR is for; the rule
      # App::karr::Role::DependencyCheck states one module over ("the human
      # copy goes to STDERR so STDOUT stays parseable") is likewise
      # unconditional, only its *suppression* depends on an option. Second, the
      # non-JSON path has the same defect in a quieter form: `karr delete 1 >
      # kept.txt` wrote the question into the file, so the operator at the
      # terminal was asked nothing and waited at a blank cursor. And third,
      # making the channel depend on a flag means the fix only reaches the
      # caller who remembered the flag.
      #
      # Rejecting `--json` without `--yes` outright was the other candidate. It
      # would have deleted a live answer: `deleted => false` with the two
      # warning keys beside it is exactly the shape #236 and #242 built for a
      # card the operator declined to delete, and under --yes there is no
      # prompt to decline at all, so that shape would become unreachable. It
      # also refuses `printf 'y\nn\ny\n' | karr delete 1,2,3 --json`, a
      # per-card answer that --yes cannot express because --yes is
      # all-or-nothing. --json names an output format and --yes a confirmation
      # policy; they are orthogonal, not the contradicting pair #235 refuses.
      printf STDERR "Delete task %d: %s? [y/N] ", $task->id, $task->title;
      # The question has to be out before the read that waits for its answer,
      # and this printf alone does not put it there: it ends without a newline,
      # so nothing in the buffering flushes it (#241).
      #
      # Whether that showed depended on where stdin came from, which is why it
      # went unnoticed for so long. With stdin on a terminal PerlIO flushes the
      # line-buffered handles when it fills its read buffer, so the prompt got
      # out by somebody else's courtesy -- a detail of the implementation, not
      # a promise. With stdin anywhere else -- a pipe, a file, an agent harness
      # feeding answers -- nothing does it, and the question sits in the buffer
      # until the next newline or process exit pushes it out, which is after
      # karr has already acted on the answer. `karr delete 1 < answers` in a
      # terminal printed the question and the outcome together at the end.
      #
      # The flush moves with the question, and what it is worth changed under
      # it: #249 turned autoflush on for STDOUT and STDERR in
      # App::karr::Encoding::enable_std_utf8, so on the CLI path the question is
      # already on the wire when this line runs, and t/241 passes without it.
      # It stays anyway, for the same reason it was written. The promise belongs
      # to the question -- it must be readable before the read below blocks --
      # and not to whatever the process-wide handle setup happens to be today: a
      # caller that reaches this command without bin/karr, an in-process test
      # that reopened STDERR onto a capture handle and so dropped both the layer
      # and the autoflush with it, or a later revert of #249, all get a buffered
      # handle back. ->flush does not care how the handle is buffered, and on an
      # unbuffered one it costs a call, so the question arrives before the wait
      # whichever way stderr and stdin are connected.
      STDERR->flush;
      my $answer = <STDIN>;

      # <STDIN> returns undef at EOF, and karr used to run straight on into
      # `chomp $answer` -- two "Use of uninitialized value $answer" warnings on
      # stderr, then "Skipped task 2: d", for every agent or CI run that forgot
      # --yes (ticket #73). What the right answer to a non-answer is depends on
      # where stdin came from:
      #
      #   a terminal   the user pressed Ctrl-D. That is "no": skip the task and
      #                exit 0, the same as typing n, and now without warnings.
      #
      #   anything else  nobody is there and nobody will be, so there is no
      #                point pretending the prompt happened. Refuse and say what
      #                to do, the way karr's other destructive commands refuse
      #                without --yes and the way kanban-md refuses when
      #                term.IsTerminal is false.
      #
      # An answer that *is* there is honoured either way, so piping "y" or "n"
      # into `karr delete` keeps working.



( run in 1.784 second using v1.01-cache-2.11-cpan-d01c6094234 )