# peer_report


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

In work together mode, sessions work while the user is away. This module
reads the log of messages and tells the user what happened.

The table, the glossary and the action items that cannot wait come from
code. The simple examples come from one `summary` model call. If that
call fails, the report is still saved, with one line that says the
examples are missing.

The module loads no model code until it needs it, so `ramabana report`
stays light.

## Threads

[`threads`](https://vedicreader.github.io/ramabana/peer_report.html#threads)
joins the log rows of one conversation. A thread starts with an `ask`.
Its state tells the user how it ended:

- `replied`: a reply with proof.
- `not proven`: a reply with no `proof`.
- `refused`: a reply that starts with `refused:`.
- `waiting`: no reply yet.
- `stopped`: a limit ended the thread.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L41"
target="_blank" style="float:right; font-size:smaller">source</a>

### threads

``` python
def threads(
    rows, known:NoneType=None
):
```

*One dict per thread in log `rows`:
`id, asker, worker, ask, state, why, folders, changes, proof`; `known`
maps keys to sessions that `rows` do not show.*

A small log with an ask that got a reply with proof, one that got none,
and an ask with no reply. The last thread was stopped by the hop limit.

``` python
test_eq([t['state'] for t in threads(LOG)], ['replied', 'not proven', 'waiting', 'stopped'])
```

The state `not proven` is the one to watch: the worker says it is done
and shows no proof.

``` python
test_eq(threads(LOG)[1]['proof'], [])
test_eq(threads(LOG)[0]['worker'], 'Ramabana')
```

Only a reply that starts with `refused:` is a refusal. An ask from
before the report, or a stop with no ask, gets no made-up asker.

``` python
test_eq(threads([LOG[0], dict(LOG[1], text='refused: not my repo')])[0]['state'], 'refused')
test_eq(threads([LOG[0], dict(LOG[1], text="I can't reproduce it, but I fixed X")])[0]['state'], 'replied')
t, = threads([LOG[1]])
test_eq((t['asker'], t['worker'], t['ask']), ('c1', 'Ramabana', NO_ASK))
t, = threads([LOG[-1]])
test_eq((t['asker'], t['worker'], t['ask']), ('?', '', 'hop limit: 6 of 6 hops used'))
```

## Checked changes

A worker lists the `changes` it made.
[`checked_changes`](https://vedicreader.github.io/ramabana/peer_report.html#checked_changes)
looks in `git log` of the worker’s folders since the mode started. An
item that git does not show gets `(not in git)`. Without folders,
nothing can be checked, so the items stay as they are.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L58"
target="_blank" style="float:right; font-size:smaller">source</a>

### checked_changes

``` python
def checked_changes(
    t, since
):
```

*The `changes` of thread `t`, each marked `(not in git)` when no commit
or file of the worker’s repos since `since` matches it; no repo, no
marks.*

A temporary repo with one commit. The real commit and file pass; the
others are marked.

``` python
with tempfile.TemporaryDirectory() as d:
    run = lambda *a: subprocess.run(['git', '-C', d, '-c', 'user.name=t', '-c', 'user.email=t@t', *a], capture_output=True, text=True).stdout.strip()
    run('init', '-q'); (Path(d)/'a.py').write_text('x'); run('add', '.'); run('commit', '-q', '-m', 'one')
    sha = run('rev-parse', 'HEAD')[:8]
    t = dict(folders=[d], changes=[sha, 'deadbeef1', 'a.py', 'gone.py'])
    test_eq(checked_changes(t, time.time() - 3600), [sha, 'deadbeef1 (not in git)', 'a.py', 'gone.py (not in git)'])
    test_eq(checked_changes(dict(folders=[], changes=['x.py']), 0), ['x.py'])
with tempfile.TemporaryDirectory() as d: test_eq(checked_changes(dict(folders=[d], changes=['x.py']), 0), ['x.py'])
```

## The table

[`table`](https://vedicreader.github.io/ramabana/peer_report.html#table)
makes one Markdown row per thread. A `|` in the text is escaped, and a
long ask is cut short.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L76"
target="_blank" style="float:right; font-size:smaller">source</a>

### table

``` python
def table(
    ts
):
```

*A Markdown table of the threads `ts`, one row each.*

``` python
test_eq(table(threads(LOG)).splitlines()[0], '| Asked by | Done by | What | Changed | Proof | State |')
test_eq(len(table(threads(LOG)).splitlines()), 6)
assert '| Leela | Ramabana | Fix card |' in table(threads(LOG))
```

## The glossary

The report uses a few terms that a new reader may not know.
[`glossary`](https://vedicreader.github.io/ramabana/peer_report.html#glossary)
gives the meaning of only the terms in a text.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L95"
target="_blank" style="float:right; font-size:smaller">source</a>

### glossary

``` python
def glossary(
    text
):
```

*The lines of `GLOSSARY` for the terms that `text` uses.*

``` python
g = glossary('One ask, one reply, one thread.')
test_eq(g.count('\n'), 2)
assert 'budget' not in g and '**thread**' in g
```

## Actions and examples

A waiting ask, a `not proven` thread, a refusal and a stopped thread
always need the user.
[`derived_actions`](https://vedicreader.github.io/ramabana/peer_report.html#derived_actions)
writes each one as “what to do: where”. The model adds more items and
one plain sentence for each change.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/monitor.py#L106"
target="_blank" style="float:right; font-size:smaller">source</a>

### summarise

``` python
def summarise(
    ts, agent:NoneType=None
):
```

*The simple examples and the action list for threads `ts`, from one
`summary` model call.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L102"
target="_blank" style="float:right; font-size:smaller">source</a>

### derived_actions

``` python
def derived_actions(
    ts
):
```

*The action lines that the table alone shows: each one is
`what to do: where`.*

The action lines of the small log. No model is called here.

``` python
test_eq(derived_actions(threads(LOG)), ["- Ask for proof of 'Fix card': Ramabana", "- Get an answer to 'Wait': Ramabana", "- Restart 'Loop' (hop limit: 6 of 6 hops used): Ramabana"])
```

A stub agent shows how the model reply is split. Its `oneshot` is the
only part of the agent that
[`summarise`](https://vedicreader.github.io/ramabana/monitor.html#summarise)
uses.

``` python
class _Agent:
    def oneshot(self, prompt, sp, job): return 'EXAMPLES\nRamabana added tell.\nACTIONS\nRead the diff: cli.py'
test_eq(summarise(threads(LOG), _Agent()), ('- Ramabana added tell.', '- Read the diff: cli.py'))
```

## The report

[`report`](https://vedicreader.github.io/ramabana/monitor.html#report)
joins the parts. The action list has the derived lines first, then the
model’s own lines without repeats. If the summary fails, the report says
why in place of the examples. With no thread, the model is not called.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/monitor.py#L112"
target="_blank" style="float:right; font-size:smaller">source</a>

### report

``` python
def report(
    since, cfg:NoneType=None, agent:NoneType=None, summary:NoneType=None
):
```

*The Markdown report of the log since time `since`; `summary` is
[`summarise`](https://vedicreader.github.io/ramabana/monitor.html#summarise)
unless given.*

A log in a temporary folder and a model that fails. The table, the
actions and the glossary are still there, with the reason.

``` python
from unittest.mock import patch
from ramabana.peers import log
```

``` python
def _down(ts, agent=None): raise RuntimeError('down')
with tempfile.TemporaryDirectory() as d, patch.dict(os.environ, RAMABANA_PEERS=d):
    for r in LOG: log(d, r)
    out = report(0, summary=_down)
    assert out.startswith('# Night of ') and f'{MISSING}: the summary model failed: RuntimeError: down' in out
    assert "Restart 'Loop'" in out and '**proof**' in out
```

## Saving it

[`save_report`](https://vedicreader.github.io/ramabana/peer_report.html#save_report)
writes the report to `reports/<date>-<HHMM>.md` in the peers folder,
named by `since`, so two modes on one date do not share a file.
[`report_path`](https://vedicreader.github.io/ramabana/peer_report.html#report_path)
gives that name, so a hook can find the file. It builds the file again
when the log has newer rows, or when the saved file lacks the examples.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L144"
target="_blank" style="float:right; font-size:smaller">source</a>

### save_report

``` python
def save_report(
    since, cfg:NoneType=None, summary:NoneType=None
):
```

*Write the report since `since` to the reports folder; a file with
examples and newer than the whole log is kept.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L140"
target="_blank" style="float:right; font-size:smaller">source</a>

### report_path

``` python
def report_path(
    since, cfg:NoneType=None
):
```

*The file in the reports folder for the report since `since`.*

A file with the examples is kept. A file that lacks them, a new log row,
or another `since` makes a new file.

``` python
ok = lambda ts, agent=None: ('- ok', '')
with tempfile.TemporaryDirectory() as d, patch.dict(os.environ, RAMABANA_PEERS=d):
    for r in LOG: log(d, r)
    p = save_report(0, summary=_down)
    assert MISSING in p.read_text() and p == report_path(0)
    test_eq(save_report(0, summary=ok), p)
    assert MISSING not in p.read_text()
    p.write_text('kept'); os.utime(p, (time.time(), time.time()))
    test_eq(save_report(0, summary=ok).read_text(), 'kept')
    log(d, dict(LOG[0], id='new', thread='new', at=time.time() + 5))
    assert save_report(0, summary=ok).read_text().startswith('# Night of')
    assert save_report(3600, summary=ok) != p
```

## The command

`ramabana report [--since T] [--save]` prints the report, or saves it
and prints the file name. `T` is a number of seconds since 1970 or a
date such as `2026-10-28 22:00`. Without `--since`, the report starts
when the last work together mode started.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/peer_report.py#L158"
target="_blank" style="float:right; font-size:smaller">source</a>

### report_cli

``` python
def report_cli(
    argv, cfg:NoneType=None
):
```

*Run `ramabana report` with the arguments `argv`; the exit code.*

``` python
with tempfile.TemporaryDirectory() as d, patch.dict(os.environ, RAMABANA_PEERS=d):
    test_eq(report_cli(['--since', 'soon']), 2)
```

`python -m ramabana.peer_report` runs the same command. The peer hooks
run it with their own Python, so an older `ramabana` first on `PATH`
does not matter.
