NAME

App::karr::Role::BoardAccess - Role providing board discovery, sync lifecycle, and task access

VERSION

version 0.500

DESCRIPTION

This role composes Role::BoardDiscovery and Role::SyncLifecycle and adds task-access methods that delegate to the store. Commands compose this role for full board functionality.

All task operations work directly against refs via $self->store->load_tasks() and similar. No temporary directory is created.

Activity logging

save_task and delete_task are the two doors a command changes a task through, guarded writes included, so they are also where the activity log is written. A command is recorded because it wrote, not because it remembered to call append_log -- before that, pick was the only command that remembered, and karr log and karr show --me ran on an almost empty log (#64).

Every command write goes through one of the two, which is the point: App::karr::Role::TaskMutation and pick hand their compare-and-swap through save_task's optional expected-OID argument instead of reaching past it to "save_task_cas" in App::karr::BoardStore, so there is no second write path to keep in step.

The action name comes from the command class ("log_action") and the actor from its --claim, the task's holder, or the Git identity ("log_agent"). Bulk paths that deliberately reinstate state verbatim -- import, restore, repair -- reach App::karr::BoardStore directly and stay unlogged.

load_tasks

my @tasks = $self->load_tasks;

In a command class that composes this role, returns every task on the board as App::karr::Task objects, in no particular order. A ref left behind by a crashed pick (e.g. an orphaned lock with no data ref) is silently excluded rather than surfaced as undef in the list -- see "load_tasks" in App::karr::BoardStore.

find_task

my $task = $self->find_task($id);

In a command class that composes this role, looks up one task by id and returns the App::karr::Task, or undef if no task with that id exists. It does not die on a missing id -- callers that need a hard failure (most of them) check the return value themselves, e.g. $self->find_task($id) or die "Task $id not found\n".

save_task

$self->save_task($task);                    # unguarded
$self->save_task($task, $expected_oid);      # compare-and-swap

In a command class that composes this role, writes a task and records the activity-log entry for it -- the only door a write goes through that also logs. With two arguments the write is unconditional; with a third it is a compare-and-swap against $expected_oid (the OID "find_task_with_oid" in App::karr::BoardStore read the task from) and returns false, without writing or logging, if another agent has moved the ref since. The guarded form is what "update_task_guarded" in App::karr::Role::TaskMutation and pick use instead of reaching past this method to save_task_cas directly, so there is exactly one write path to keep the log in step with.

delete_task

my $ok = $self->delete_task($id);

In a command class that composes this role, deletes a task's ref and records the activity-log entry for it, mirroring "save_task" as the other of the two doors a command writes through. Returns whatever "delete_task" in App::karr::BoardStore returns, and logs only when that is true: a delete of an id that was never there removes nothing, so it leaves no entry behind, exactly as save_task leaves none for a write that lost its compare-and-swap (#120).

allocate_next_id

my $id = $self->allocate_next_id;

In a command class that composes this role, reserves and returns the next free task id, delegating to "allocate_next_id" in App::karr::BoardStore. The allocation is a compare-and-swap on the board's counter ref, so two agents running karr create at the same time are always handed different ids.

parse_ids

my @ids = $self->parse_ids('1,2,3');   # (1, 2, 3)
my @ids = $self->parse_ids('7');       # (7)

In a command class that composes this role, splits the comma-separated id argument every batch-capable command (move, edit, delete, archive, unlock) takes on its single positional and returns the ids in order, unvalidated and as plain strings. There is no range syntax (1-3) and no whitespace handling; an empty string returns an empty list. Whether each id actually names a task is left to the per-id callback each command runs via "run_batch" in App::karr::Role::TaskMutation.

activity_log

my $log = $self->activity_log;              # this command's own git/role
my $log = $self->activity_log($other_git);   # a different repo

In a command class that composes this role, builds an App::karr::ActivityLog for $git (defaulting to $self->git) and this command's role. Most callers use it to read ($self->activity_log->entries); writing a mutation normally happens through "save_task" or "delete_task" instead of this method directly -- see "Activity logging" above.

append_log

$self->append_log($self->git,
    agent   => $self->claim,
    action  => 'pick',
    task_id => $picked->id,
    detail  => $picked->status,
);

In a command class that composes this role, writes one activity-log entry. Unlike "save_task" and "delete_task", this is never called automatically by a write -- a command is recorded because it wrote through one of those two doors, or because it called append_log itself, as pick does here for the claim it takes outside the guarded save. $git is required (no default) and %entry is handed to "log_entry" in App::karr::ActivityLog unchanged. Guarded against double-logging the same action/task_id pair within one command run, the same guard "log_task_write" uses.

log_action

The action name recorded for this command's writes: the class's own name segment, hyphenated (App::karr::Cmd::AgentName gives agent-name). Naming the action after the command is what lets a new mutating command be logged without opting in.

log_agent

Who a log entry is attributed to: this command's --claim if it takes one, else whoever holds the task, else the Git identity behind the board.

log_task_write

$self->log_task_write( $task->id, $task->status, $task );

Records one task mutation in the activity log. Called by save_task and delete_task; at most one entry per action and task id per command run.

Writing the config

There is deliberately no save_config on this role. Config writes go to $self->store->save_config($effective_hash) directly, as Cmd::Config and Cmd::Init do -- unlike "save_task" and "delete_task", which earn their door by writing the activity log, a role-level wrapper would add nothing to the store's own method.

The one this role used to carry defaulted its argument to $self->config, which is an App::karr::Config object, while "save_config" in App::karr::BoardStore takes the plain effective-config hash: it reads $effective->{version} and diffs the whole thing against the defaults. Handed the object it saw the data and file keys of the blessed hash instead, and because that merges over the defaults into something schema-valid, nothing refused it -- $self->save_config wrote a refs/karr/config whose entire real content sat nested under a data: key and whose board.name was gone. Nothing in lib/ or t/ ever called it, which is the only reason no board was ever corrupted this way (#120).

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.