App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/BoardStore.pm  view on Meta::CPAN


1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::karr::BoardStore - Ref-backed board storage for karr

=head1 VERSION

version 0.600

=head1 SYNOPSIS

    my $store = App::karr::BoardStore->new( git => $git );
    my $config = $store->load_config;
    my $id = $store->allocate_next_id;
    my @tasks = $store->load_tasks;

=head1 DESCRIPTION

L<App::karr::BoardStore> treats C<refs/karr/*> as the canonical board state.
It can merge sparse config overrides with code defaults, allocate numeric task
ids through a dedicated metadata ref, and materialize or serialize temporary
board views for command handlers that still work with files internally.

=head2 git

The L<App::karr::Git> instance this store reads and writes board refs
through. Required.

=head1 SEE ALSO

L<karr>, L<App::karr>, L<App::karr::Git>, L<App::karr::Task>,
L<App::karr::Config>

=head2 board_exists

True when this repository holds an initialized board, which means exactly one
thing: C<refs/karr/config> is there. It used to accept C<refs/karr/meta/next-id>
on its own as well, and that is how a stray C<karr create> in the wrong
directory produced a board that C<karr init> then refused to touch for good --
the half-board counted as existing, so the name, the statuses and the
F<.gitignore> entries could never be written (#62).

    my $whole = $store->board_exists;

Callers state the refusal through L<App::karr::Role::BoardDiscovery/require_board>
rather than testing this themselves: a repository that fails this check may
still hold a half-board's tasks, and the two cases need different words (#133).

=head2 has_board_refs

True when anything at all lives under C<refs/karr/>, initialized board or not.
This is the question the commands that clean up or read raw refs
(C<backup>, C<destroy>, C<materialize>, C<repair>) actually have: refusing them
on a half-board would strand the refs a pre-fix karr already left behind, with
no way to remove them from inside karr.

    my $anything_here = $store->has_board_refs;

=head2 load_config_overrides

Returns the board's raw config overrides -- whatever C<refs/karr/config>
currently holds, decoded but not merged with the code defaults. A board with
no config ref yet, or one whose ref does not decode to a mapping, answers
C<{}> rather than C<undef> or dying.

    my $overrides = $store->load_config_overrides;   # sparse, not effective

This is the input L</load_config> merges over
L<App::karr::Config/default_config>; see that method for the merged result,
and L</effective_config> for its cached form.

=head2 load_config

Reads L</load_config_overrides> and merges them over the code defaults via
L<App::karr::Config/effective_config>, returning a plain hash reference --
not a blessed L<App::karr::Config> object. Every call re-reads the config
ref; L</effective_config> is the cached wrapper most callers want instead.

    my $ec = $store->load_config;

=head2 effective_config

The board's merged config, cached for the lifetime of this C<$store>
instance. The first call runs L</load_config>; every call after returns the
same hash reference until L</save_config> invalidates the cache. This is the
entry point almost every command and role uses --
C<< App::karr::Config->from_merged( $store->effective_config ) >> is the
standard way to get a queryable L<App::karr::Config> object for the current
board (see L<App::karr::Config/from_merged>).

Not to be confused with the class method
L<App::karr::Config/effective_config>, which does the actual default/override
merge and takes no board at all; this method is the per-store cache built on
top of it.

    my $ec = $store->effective_config;
    my $config = App::karr::Config->from_merged($ec);

=head2 all_status_names

Returns a list of all status names from the effective config.

    my @statuses = $store->all_status_names;

=head2 status_requires_claim

Returns true if the given status requires a claim.

    if ($store->status_requires_claim('in-progress')) {
        # must use --claim to move here
    }

=head2 is_terminal_status

lib/App/karr/BoardStore.pm  view on Meta::CPAN

L<App::karr::Git/is_tracked_under> answers the whole question in one index
read, for a file or a directory alike -- F<tasks/notes/old.md> makes
F<tasks/> just as owned as F<tasks/README.md> would. It used to be answered by
walking the working tree and asking L<App::karr::Git/is_tracked> per file
found, which cost one status call per card and, more importantly, could not
find a path git tracks but that is currently missing from the working tree --
there is no file there to walk onto. Both are fixed by asking the index
directly instead (#104).

=head2 ensure_gitignore

Idempotently appends any of L</file_view_gitignore_entries> missing from
F<$board_dir/.gitignore> (creating the file, and a header comment, on first
use). Returns the list of entries actually added -- empty when the file
already covers everything.

This method does B<not> itself check whether the project already tracks
content at those paths; it only ever appends. The check is
L</project_owned_view_paths>, a separate call so a caller can ask before
writing anything: C<karr init> and C<karr materialize> both call it first and
skip C<ensure_gitignore> entirely when it returns anything, because appending
an entry for a path git already tracks would be inert at best and misleading
at worst (tickets #48, #89, #100, #104, #107).

    my @owned = $store->project_owned_view_paths($board_dir);
    my @added = @owned ? () : $store->ensure_gitignore($board_dir);

=head2 serialize_from

Reads a file view at C<$board_dir> back into C<refs/karr/*>: task refs are
replaced by the cards, refs the view does not mention are pruned, and the id
counter is moved up to whichever is higher, the highest imported id plus one or
the view's own C<next_id>. It is never moved down -- an id another tool has
already handed out must not be handed out again (ticket #90).

The config is reconciled rather than replaced. Tasks are the whole truth of the
file view; its F<config.yml> is not, because anything that loads the view may
rewrite it into a schema of its own. So the view speaks only for the keys it
carries and karr models, and C<refs/karr/config> keeps the rest -- see
L<App::karr::Config/reconcile_view_config>.

All or nothing. Every card is parsed before the first ref is written, so a
malformed file aborts the whole import -- listing each rejected file and its
reason -- with the board left exactly as it was (ticket #70). Refusing an empty
view is the caller's job; see L<App::karr::Cmd::Import>.

Importing into a repository that held nothing under C<refs/karr/> also creates
the board: it writes a config when the view has none, seeds the counter, stamps
the board identity, and stamps the encoding marker. Importing into a board that
was already there does B<not> stamp the marker -- see
L</stamp_encoding_version>.

    $store->serialize_from( $git_root );

=head2 snapshot

Reads every ref under C<refs/karr/> into a plain hash reference:
C<< { version => 1, refs => { $ref_name => $content, ... } } >>, where
C<$content> is that ref's raw stored text (config YAML, a task's Markdown
document, or a bare id/hex string for the meta refs) via
L<App::karr::Git/read_ref>. C<karr backup> writes this straight to YAML; pair
with L</restore_snapshot> to write one back onto C<refs/karr/*>.

    my $snapshot = $store->snapshot;

=head2 restore_snapshot

Makes the board consist of exactly the refs in the snapshot. Every ref name is
checked and every commit object built before the first ref moves, so a snapshot
karr cannot write is refused with the board untouched instead of destroying it
on the way through (#47). See L<App::karr::Git/replace_board_refs>.

    $store->restore_snapshot( $snapshot );

=head1 SUPPORT

=head2 Issues

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

=head2 IRC

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

=head1 CONTRIBUTING

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

=head1 AUTHOR

Torsten Raudssus <getty@cpan.org>

=head1 COPYRIGHT AND LICENSE

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

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut



( run in 0.983 second using v1.01-cache-2.11-cpan-4ef0a570458 )