Investigation reports¶
Every surface in this tool produces evidence. A report produces the account of it: "across these records, these nodes and this window, here is what happened and why" — kept, re-checkable, and shareable, instead of living in a chat window that scrolls away.
References, never a document¶
A report is an ordered list of references with connective prose — never a free-form document.
A finding flagged as kind: "confirmation" uses Observation / Assessment, and one of the neutral kind
note (what flag saves when no kind is given) uses Note / Assessment, including in exported
PDFs. Fault findings keep What is wrong / Likely cause. Choose the category when writing the flag; the report
uses that same finding.
Sections are typed: a finding points at a flagged record, a record at a cycle, a chart at a
named graph, a topology at a named focus, a table at the query that derives its rows, and only
narrative stores its own content. That one constraint keeps this a forensic instrument: a report
assembles what other verbs produced, and every claim traces to something that can be checked.
Two rules follow, and both are enforced rather than encouraged:
- A report includes findings; it never authors them. A finding section renders what
flagwrote, byte-identical — the report verb has nowhere to put its own version of the diagnosis, so one record can never carry two accounts of itself. - Narrative is visibly narrative. Prose renders with its own colour and a standing label — "the author's account, not log evidence" — on screen and in the PDF alike, so an assertion can never pass as a record.
Evidence re-renders live¶
A report stores references, so opening it re-reads the records, re-extracts the charts and re-runs the queries against the loaded log. A reference that no longer resolves is reported — "2 of 5 anchors did not resolve against this log" — never silently dropped or silently stale. That also makes a report a re-runnable claim: open last month's investigation against today's log and find out whether it still holds.
Because the dangerous failure arrives resolved rather than dangling (record #42 exists in any log with 43 records), a report also captures its authoring context: a fingerprint of the log it was written against (name, record count, time range — and, where the log's provenance came from a project environment rather than a declaration, matched by directory, not declared) and the filter it was written under. Opening it against a different log names the mismatch before any section renders — announced, never refused, because comparing a finding against a later run is legitimate and valuable. Opening it under a different filter offers the stored view; declining renders under the current filter and says so on the page.
Tables: derived rows, declared presentation¶
A table section stores the query that produces its rows plus a column spec — headings, order,
widths, alignment, number formats (0.00, percent, duration, time). Rows re-run with the log;
the presentation persists. Numbers set right-aligned in tabular figures. A table can cite the same
verbs an agent sees over MCP:
read {fields, recordIndex…}— the selected records;aggregate {metric, groupBy, filter, limit}— count/rate buckets;series {expr, buckets}— buckets,series {expr, crossings}— anchored crossing rows, or plainseries {expr}— one statistics row;coverage {filtered}— the full graph-ordered ledger: covered, uncovered, and deliberately excluded nodes with their reasons.
Each source prints its own scalar context under the table — population and scope for an aggregate,
points and thresholds for a series, or the complete coverage denominator — and the same line appears
in the PDF. A zero-result call keeps its header and says why it has no rows. Structured call values
such as filter: {from, to, dimensions} are saved and reissued as data, rather than becoming display
text after a restart.
A row highlight is a rule, not a paint: rowWhen: "book.mid > 17" re-evaluates with the data,
strictly against each row's own record — a value the record didn't log cannot fire the rule —
and the rule is printed with the table, because an unexplained red row would be a judgement
wearing evidence styling. That makes it valid for read and series-crossing rows (which link back to
a record), but not aggregate buckets, series buckets/statistics, or coverage rows; those refuse the
rule with a visible warning rather than quietly applying a different column formula. Tables export to
CSV (raw values — formatting stays on the page).
The Reports tab¶

Reports appear in the Reports tab: pick one and its sections render in order, each evidence section clickable through to the thing it references — a finding selects its record, a chart opens that graph, a focus applies on the topology. A report is a navigation surface, not just an output.
Beside the reports, the Spotlight walks list holds the walks saved in this project — step-by-step spotlights you replay with ◀ ▶. Use Play or select a step and choose Play selected step; Conversation…, Record reel…, Rename, Delete and Restore deleted… are under More. They are stored and shared the way reports are; Save it as a walk says how to make one, and Send it as a reel turns one into a page you can send to somebody without the analyser — ending on the evidence bundle behind it, or on a plain statement that there is none.
Building one¶
Agents (or you, over REST) use the report verb:
{"name": "oversell-inv", "title": "Oversell investigation",
"sections": [
{"kind": "finding", "recordIndex": 99},
{"kind": "chart", "graph": "stock vs revenue"},
{"kind": "table", "call": {"verb": "read", "fields": "stockLedger.onHand, till.gross"},
"rowWhen": "stockLedger.onHand < 0", "rowWhenLabel": "oversold"},
{"kind": "narrative", "text": "Revenue is priced from the request, not from what the shelf could supply."}
]}
Re-issuing with the same name updates the report, and only what the call names changes. With
sections it replaces them, along with the log and view they were written against and the created date.
Without them, or with sections: null, the sections are kept, so a call that only retitles a report or
adds a path cannot empty it. sections: [] empties a report deliberately; the reply's
sectionsKept says when sections were carried over. Add path to render the PDF, or
csv: 2, path: "rows.csv" to export that table's rows — both write inside the exchange directory
under the same Allow assistant file exchange opt-in as every other file the assistant writes.
The Reports tab has the same door for you: Export PDF… renders the selected report to a file you
choose — your chooser is the consent, so it is not confined to the exchange directory.
Removing and renaming one¶
Replacing by name is how you update a report. Getting rid of one is separate, because an investigation leaves diagnostics behind — the two-section report built to check whether a topology picture renders should not still be in the profile you hand to a colleague, sitting in the list looking exactly like a real finding.
Delete… and Rename… are under More beside Export PDF… in the Reports tab and act on the selected report. Delete confirms first, naming the report, the log it cites and how many sections go; the log, the charts it referenced and any PDF you already exported are untouched — a report is an assembly of references, and deleting the assembly is not deleting the evidence. Rename refuses a name that is already taken rather than quietly replacing that report.
A delete can be undone. A deleted report moves to a recently-deleted list; Restore deleted… brings it back into the project it came from. The list is kept on this machine, not in the project profile — a deleted report is never committed or shared — and holds the last 20 deletions. Restoring onto a name that has since been taken is refused, so nothing is replaced. The limit is shared across all profiles on this machine. Entries are associated with the profile's path: two profiles in one project have separate lists; moving or renaming the profile does not migrate its deleted reports.
Assistants get the same: report {name, delete: true}, report {name, rename: "…"}, and
report {restore: "<name>"} (boolean restore: true lists what can be restored), so one that built a
diagnostic can clear it away when it is done — and anything it deletes can be brought back.
Use restore alone; combining it with sections, a path or another operation is refused without
changing the report or bin. A string always names a report, including restore: "true".
Reports persist with your profile, travel with projects, and share under their own category — because a shared report carries narrative written about your data, which deserves its own consent checkbox. See Sharing settings with a file.