Skip to content

feat: 2.0.0 "CyberFraud Edition", four knowledge bases on one canvas - #11

Merged
pakdekro merged 26 commits into
mainfrom
feat/f3-fraud-framework
Aug 30, 2026
Merged

feat: 2.0.0 "CyberFraud Edition", four knowledge bases on one canvas#11
pakdekro merged 26 commits into
mainfrom
feat/f3-fraud-framework

Conversation

@pakdekro

Copy link
Copy Markdown
Owner

Four knowledge bases on one canvas, and a case that can cross between them.

What is in it

  • MITRE F3 next to ATT&CK rather than instead of it, meeting it on the technique: F3 reuses 43 ATT&CK numbers, our identifiers derive from the number alone, so the dataset build takes ATT&CK's name back for the shared ones and a test holds that line.
  • ATLAS (AI systems) and AADAPT (digital assets), neither of which borrows a number, plus the rest of ATT&CK: Mobile and ICS, merged into one corpus with a collision check at every build.
  • A framework registry: adding a fifth is an entry in frameworks.ts and a build script, not a change spread across the app. The recipe is written down in docs/adding-a-framework.md.
  • A technique now says which knowledge base its number belongs to, on the card and in the export. Absent still means ATT&CK, so nothing already on a canvas changed meaning.
  • A guide page for each of the four, prerendered like the others.
  • Ten more scenarios, fraud and AI, and a second scenario now completes the first instead of drawing a second graph beside it.
  • The narrative reads the case in the order it happened: a global chronology, the hour when it is known, per-subject timelines in a report, and the undated blocks under their own heading.
  • At export, the chronology drawn as a timeline in all four formats, optional, and the PDF laid out as a document: the graph takes a page, the prose is portrait, a section starts its own page.

Compatibility

Nothing breaks. Bundles stay valid, canvases written by 1.x open exactly as they did, the file format has not moved. The major number is about what the tool is for.

Checks

698 vitest, tsc --noEmit, eslint and the production build all clean. Modelled end to end in the browser on a 24 object / 32 relation case crossing the four frameworks: bundle validates, no internal key leaks, roundtrip exact.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH

pakdekro and others added 26 commits August 29, 2026 23:23
F3 is offered next to ATT&CK in the framework panel, not as a separate
mode. It needs no new verb: its bundle carries only `subtechnique-of`,
which describes the catalogue and never reaches a canvas. On the canvas a
fraud technique is an `attack-pattern` reached through the `uses` and
`targets` the matrix already allows.

The dataset is distilled from the NATIVE F3 file rather than its STIX
bundle, because only the native one carries `isAttack`: 43 of the 123
techniques are ATT&CK techniques reused by number, and the STIX bundle
stamps every external reference `mitre-f3`, T-ids included. Reading the
bundle we would end up claiming that T1566 is an F3 identifier.

Those 43 are emitted as ATT&CK techniques, so a technique reached through
either palette is ONE object in the bundle. That is what lets a fraud case
and an intrusion case meet. It also forces the naming: our identifiers
derive from the MITRE number alone, and F3 spells sub-techniques by full
path where ATT&CK spells the leaf, so the build takes ATT&CK's name back
and `f3.test.ts` holds that line against future regenerations.

The export no longer stamps `mitre-attack` on every technique carrying a
number, which was true only while ATT&CK was the sole source and becomes a
fabricated reference the moment F1001 goes out. The framework is recorded
next to the number, set by whichever palette created it, editable in the
inspector, and read back on import. Absent still means ATT&CK, so nothing
already on a canvas changed meaning and the golden vectors are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The tactic chips let you browse F3 by tactic when the search box was empty,
on the grounds that a framework you do not know yet cannot be searched. The
reasoning holds and the interface still lost: one panel, one switch, and two
different things happening depending on the side, discovered by clicking.

The dataset keeps its tactics. They are the only thing that makes this
framework legible to someone meeting it, and that belongs on a page with
room to explain rather than in 180 pixels of palette.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
… two

The dropdown offered seven observables where `patternFromObservable` handled
thirteen. The second batch (accounts, software, certificates, MAC, mutex,
directory) reached the generator through the inspector's "Generate an
indicator" button and never reached the builder, so a pattern that was
perfectly expressible looked impossible to write by hand.

That is worse than a missing feature. Nothing was broken and nothing warned:
an analyst concludes the format cannot say the thing and models around it. A
sweep in `pattern.test.ts` now holds the dropdown against SCO_TYPES, the way
`bridges.test.ts` holds the recipes against the matrix.

An account gets two entries rather than one, because `account_login` and
`user_id` are not interchangeable: the spec defines the login as what the
holder types to sign in. Writing a bank account number in there asserts that
someone signs in with their IBAN. The value is passed empty when it belongs
in a property, since the resolver reads the observable's value first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The narrative sorted the story by type: actors, tooling, infrastructure,
observables. That is a reasonable guess when nothing better is known, and
it is exactly wrong for a fraud, where the case IS the sequence: the eight
dated steps of a real BEC came out as a bag.

A relation's start_time is the analyst saying when a thing happened, so it
outranks the guess. A dated statement now says its day and takes its rank
from it, an undated one keeps its place ahead of the sequence, and the
rule holds inside a block as well as between them. Clauses group by verb
AND day, because one date on two days would be a claim nobody made.

A graph with no date is untouched, and a test holds that.

Folded in: an F3 external reference now points at the hash route the F3
site actually serves, not the flat path F3's own bundle publishes and
nothing answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
… from

A case that crosses from an intrusion to a fraud carries techniques from
both matrices, and on the canvas they were indistinguishable: same shape,
same colour, same word "Technique".

The mark is read off the property and never off the number, which cannot
arbitrate in either direction: F3 reuses 43 ATT&CK numbers, and publishes
T-numbers of its own that an import can legitimately bring in as F3.

Only F3 is marked. Absent means ATT&CK everywhere else in the app, most
graphs are made of ATT&CK, and a chip on every technique would say nothing
on either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Three things the fraud case surfaced, all in the same corner of the app.

An email address reached the indicator bridge and nothing else, with none
of the reasons mutex and directory have for staying out: an address you
send phishing from is something you acquired, like the domain that hosts
the page it links to. It joins the infrastructure parts.

The part recipe now words itself after the observable. STIX has no SDO
that carries a bank account, `infrastructure` is the closest and it is the
object that makes one detectable on a platform that only acts on objects,
but "infrastructure this observable is part of" offered over an IBAN reads
as a category error and the analyst walks away from the one route that
works. Same object, same bundle, different question.

And the infrastructure types are offered at last, from the vendored OASIS
schema, with a test holding the list in step. The schema declares plain
strings and polices nothing, so the form is where the line holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
…h one it is

Both frameworks are named on a card now, not just the newcomer: marking the
exception works while there are two and reads as an oversight the day there
is a third. The export, the form and the card read one registry, so adding
a framework is one entry. Ctrl+K gains an F3 group, F3's own techniques
only: the 43 it borrows are already under ATT&CK and build the same object.

And an account name says which of the three names it is. account_login is
what you type to sign in, user_id is what the system calls the account, and
an IBAN written into a login was a fabricated claim whose identifier
belongs to an account that does not exist. The default stays account_login
so nothing already drawn moves, the form hides the field the name occupies,
the generated pattern follows the same choice, and the import records what
it read - an account known only by its user_id used to come home as a
login, which is another account.

A display name identifies nobody. STIX answers that with a random
identifier, which is the one thing this application cannot do, so the
export refuses and the lint says so first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The palette finds F1005.003 and says nothing about what F3 is. That is
fine for ATT&CK, which is common knowledge in this trade, and no use at
all for a framework that is a year old: a framework you do not know yet
cannot be searched, and the reading does not fit in 180 pixels of palette.

So two pages of their own, /attack and /f3, and not a section of the STIX
guide: that page teaches a FORMAT, these teach a BODY OF KNOWLEDGE, and
the reader of one is rarely the reader of the other at the same moment.
What the framework is and is not, how to read a number, the trap that F3
also publishes ATT&CK numbers, and what a technique becomes once it is
here.

Derived where it can be: the relationships come from the matrix, like the
rest of the guide. Written down where it must be, since the frameworks
live outside this repository, and held in step by a test - the eight F3
tactics on the page are the eight in the shipped dataset, or the build
says so.

Same shape as /about: prose, no script, prerendered at build time, and the
same component served inside the app at #/attack and #/f3. VerbList moves
out of the guide to be shared rather than copied.

The way in is two named links under the search box, under a rule, both of
them whichever framework is selected: an icon beside the title had to be
found and then guessed at, and "what is the other one" is exactly the
question somebody in front of that switch is asking. A new tab, because
the reader has a canvas open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The F3 build read a pinned `f3-v1.1.json`. The native series is published
one file per version with no unversioned copy, so a v1.2 release would have
shipped and the Monday job would have kept rebuilding v1.1 and reporting
"unchanged" about a framework that had moved. It now lists what MITRE
published and takes the highest version, comparing numbers as tuples so
that f3-v1.json does not outrank f3-v1.1.json, and names the file it read.

Deliberately not `src/data/matrix-data.json`, which is what their site
builds from and is always current: it is working state on a branch, and we
ship what MITRE published.

The weekly job also gained a reason to keep going when the tests fail. What
fails there is a disagreement between the regenerated files and something
written down beside them - a name arbitration, or now a number in the
framework pages, which `frameworks.static.test.tsx` holds against the
datasets. The fix belongs on that branch, next to the diff that caused it,
so the pull request is opened either way, says which of the two happened,
and the run then goes red so the notification still fires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Written straight after F3 went in, while the order of the work and the
reasons behind each choice were still exact. Four questions to answer from
the framework's published data before writing anything, the files to touch
in order, and the traps that have already been paid for: a STIX bundle that
loses the provenance its native file carries, a pinned version file that
makes the weekly refresh blind, two spellings of one number that put two
cards on the canvas, and an identifier shape that decides nothing.

CONTRIBUTING points at it, and its dataset section now counts four corpora
rather than three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
…othing

MITRE's Adversarial Threat Landscape for AI Systems: 178 techniques, 16
tactics, beside ATT&CK and F3 in the same panel and on the same canvas. An
intrusion that ends in a poisoned model is one graph.

Where F3 reuses 43 ATT&CK numbers, ATLAS reuses none: everything is an AML
number of its own, and the 37 techniques adapted from an ATT&CK one record
which, in a field the page reads and no bundle ever claims. MITRE's own
ATLAS bundle does not claim it either, which settles it. The 36 techniques
whose name also exists in ATT&CK are two cards with two numbers, which is
what they are, and the mark on a card is what says so.

The framework itself cost a build script, a registry entry, a loader and a
page. Nothing else moved, and that was the point of the previous commits:
both palettes, the export, the import and the extraction read the registry,
so the switch, the search, the Ctrl+K group, the reference written and the
one read back all came for free. Two hardcoded `mitre-f3` comparisons were
the only holdouts, and they were bugs waiting for exactly this.

The build follows MITRE's own pointer chain to the current release rather
than pinning a file, ATLAS shipping one every month or two. Left out and
said so on the page: 37 mitigations, which are course-of-action objects
this canvas has no card for, and 68 case studies, which are not STIX.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The palette shipped Enterprise and nothing else, so a SIM swap or an attack
on a PLC had to be typed into the MITRE ID field by hand.

The three are merged into one corpus rather than put behind a selector,
because they are one knowledge base: one identifier space, one
`mitre-attack` reference, and not one number in common. That last part is
the only thing that could have made this hard, so the build checks it at
every run instead of trusting today's answer: the same number under two
names would put two cards on the canvas for one identifier.

918 techniques instead of 697, plus 126 malware families and 6 groups. A
result that is not Enterprise carries its matrix, in the panel and in
Ctrl+K: T0800 answers a question nobody working an office intrusion asked.

Two consequences worth naming. Each domain names its own kill chain, so
reading only `mitre-attack` would have stripped every Mobile and ICS
technique of its tactics. And the corpus growing moved five actors out of
the MISP galaxy, which ATT&CK now resolves itself, while three of the five
identifiers F3 flags as ATT&CK and could not resolve turn out to be Mobile:
they are checked against ATT&CK now rather than trusted, and the spellings
agreed all along, which is precisely what nobody could tell before. The
datasets move to v19.2 in the same pass.

Cloud, Containers, ESXi and Network Devices are NOT domains: they are
platforms of Enterprise, and their matrices on the website are filtered
views of a corpus we already ship whole. Said on the page, so nobody goes
looking for a dataset that does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
MITRE's Adversarial Actions in Digital Asset Payment Technologies: 68
techniques over eleven tactics, about exchanges, wallets, bridges and the
consensus underneath them. It belongs here for one reason above the others:
F3 ends at Monetization with "Convert to Cryptocurrency", and AADAPT's own
eleventh tactic is called Fraud. A case can now run from a phishing message
to a wire transfer to a bridge without leaving the canvas.

It is also the hybrid of the two arrangements already shipped. Ten of its
eleven tactics ARE ATT&CK tactics by identifier, the way six of F3's eight
are, which costs nothing since nothing here creates a tactic object. Every
technique is its own, the way ATLAS's are, and the four adapted from an
ATT&CK technique are numbered after it (ADT1195 from T1195) without ever
claiming to be it: the export writes mitre-aadapt and nothing else.

Two traps in their data, both paid for once. Their `ATT&CK-reference`
repeats the AADAPT identifier in its id field, so the ATT&CK number is only
readable from the url. And their file says `version: 4.4.0`, which is the
ATLAS data version their tooling was forked from, along with its CHANGELOG
and its LICENSE: the repository has no tags and no releases. This dataset
therefore ships no version at all, the panel shows none, and a test forbids
4.4.0 from appearing anywhere in it.

Licence: Apache 2.0 on the data, public release unlimited, case 25-1204.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Four catalogues now share one identifier space in this application and an
identity derived from a number alone, so what matters is no longer what
each does but what they do to each other. The audit is kept as a test:
no number claimed by two frameworks under two names, every number the
palettes produce accepted by the form, every technique exporting the
reference its framework claims and nothing else, all of it validating
against the OASIS schemas, and export-import-export stable. It held.

Two things it found, both fixed here.

The form did not check a number against the framework beside it. The shape
cannot say which framework a number belongs to, since F3 publishes ATT&CK
numbers, but the reverse direction is decidable: ATT&CK publishes no `AML.`
number, so an ATLAS number left under ATT&CK was about to leave as a
fabricated ATT&CK reference. The shapes moved into the registry, the
warning names the framework that does publish it, and it never blocks.

And the canonical bridges offered a detection indicator for an account
named by its display name, which identifies nobody: no pattern could be
built, so the recipe promised in one click an object the export refuses.
A second problem where the analyst had one. The bridge is now offered only
when there is a pattern to put in it.

Also corrected: docs/identifiers.md said the ATT&CK id wins for an
attack-pattern, which has been four frameworks' numbers for a while.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
…e first

The scenarios were all intrusions. Three fraud ones now: account takeover,
card data theft and reuse, and crypto cash-out, written on F3 and AADAPT
numbers with the framework named beside each. They live under a heading of
their own, with FOVI moved in beside them where it always belonged.

And the answer to the question that came with them, which was no. Two
templates naming the same actor drew two cards, and that is worse than
untidy: both collapse onto one STIX identity at export, so the second
card's description was quietly the one that lost, with a warning nobody
reads until the end. A scenario now attaches to the objects already drawn,
which is the rule the document import and the enrichment already follow,
and the labels of the second scenario are added to what it reuses so an
actor in two cases carries both. A relationship both scenarios assert is
drawn once.

The decision is a pure function tested on the real templates rather than
logic buried in a click handler: what to create, what to reuse, which
labels to add, which links are new.

Verified in the browser on the two that share an actor and a victim: nine
objects and eleven relationships instead of eleven and twelve, one
connected graph, and no duplicate card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Three was a floor rather than the answer. The fraud family now covers the
three ways money actually leaves: a transfer the victim makes themselves
(the fake adviser call, which compromises nothing at all), one made in
their name (account takeover, SIM swap, card reuse), and one that never
touches an account (the mule network, crypto cash-out, the token scam).

Each is a different shape rather than a re-skin: the vishing one has no
compromise in it, the SIM swap enters through the operator, the mule
network is the cash-out layer alone with one account per line, and the
token scam happens on the chain rather than beside it.

With them, a test that holds every number a scenario carries against the
shipped corpora, name included: a `fixed` technique is a number typed by
hand into a JSON file, the identifier derives from it, and nothing
downstream would catch a typo. It found one on its first run, in a
scenario that predates all of this: FOVI pointed at T1656, which ATT&CK
revoked in favour of T1684.001. Corrected, and it would have gone out as a
reference to a technique MITRE no longer publishes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
…ee families

ATLAS shipped this afternoon with nothing using it. Three scenarios now:
prompt injection against an agent that leaks through its own tools, a
poisoned model taken off a hub, and a model extracted through the API it is
served from. Each is a different shape rather than a re-skin, and the
second one is the only scenario in the set whose payload is a file with a
hash.

A fourth crosses on purpose and sits with the frauds, where the case
belongs: a deepfake-assisted transfer is ATLAS for the face and the voice,
F3 for the money. The family says what kind of case it is, never which
framework it draws on.

And the panel had to change shape for it. Twenty-six scenarios under three
headings meant scrolling past two families to reach the third; it now
switches between them behind the same chips the framework panel uses two
icons above, each carrying its count. One line per family rather than three
columns: "AI systems" does not fit in a third of a 180 pixel panel, and
three truncated words are worse than three lines.

A test refuses a family holding a single scenario, since a heading over one
thing is a heading over nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Ordering the dated statements in place was half an answer. It kept each
subject's timeline inside that subject's block, so a case whose actor and
whose malware are both dated had two timelines to interleave by eye. And it
let the undated bulk a scenario generates - nine "the technique targets the
victim" - open the story ahead of what actually happened, which is what
modelling a full case in the browser made obvious.

So two named parts. A Chronology carrying every dated statement from every
subject on one timeline, each line saying who did what that day, and then
Undated, in the reading order of the attack chain it always had. Naming
them is the point: an order the reader has to infer is an order they can
misread.

A report carries one thing the panel does not: the same chronology again,
one timeline per subject, and only when several subjects are dated. One
actor doing everything would print the same list twice under two headings,
and a 300 pixel panel read while working wants the case rather than the
case and its index. All four renderers - panel, Markdown, report image, PDF
- go through the same two helpers, so the sentence and its capitalisation
are decided once.

A graph nobody dated has no chronology, hence no headings: it reads exactly
as it did before, and the test that says so is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Exporting a real case showed it: three of the six per-subject timelines
held a single line each, which is not a sequence and reads as padding
between the two that are. A subject now needs more than one dated event to
get a timeline of its own, and the section still needs two such subjects to
appear at all. Its events are in the global chronology either way, which is
where a lone dated fact belongs.

On the case it was found with, the section went from six subjects to the
two that have a story: the actor, and the cash-out chain that gained an
IBAN on the 19th and a wallet on the 24th.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Dropping it made the by-subject section lie: a reader looking up what the
domain did, finding no entry, concludes it did nothing dated. Every dated
subject is listed again, and the one with a single event reads as a line
rather than as a heading over a list of one, which is the rule the story
blocks have followed since they were written.

The section still needs two subjects to appear at all, for the reason it
always did: one actor doing everything would print the same list twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
STIX has no day. `start_time` is an RFC 3339 timestamp with seconds and a
`Z` required, so our day-only window has always gone out as midnight UTC,
and an imported hour was worse off than that: truncated to its day on
screen, and overwritten the moment somebody edited the date.

A relationship now carries an optional time beside its date. Two fields
rather than one `datetime-local`, because leaving the hour empty has to
stay a way of saying "that day" instead of a way of asserting midnight,
which is the same distinction this application keeps everywhere else. The
chronology orders, groups and reads to the minute: two transfers on one day
are one line only when nothing distinguishes them, and an hour
distinguishes them.

Midnight exactly is read back as a day. Everybody, this application
included, writes T00:00:00Z for "that day, hour unknown", so reading it as
an hour would turn every imported day into a claim nobody made and would
make our own export-import cycle reword the narrative.

And the chronology can be drawn: an option at export puts a mermaid
timeline above the list in the Markdown, where a diagram is generated
rather than composed. It is a shortening and behaves like one, dropping the
scheme of a URL, whose colons are the diagram's own syntax, and cutting a
label past seventy characters. The list under it is the same chronology in
full: that one is to read, the drawing is to look at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
React Flow calls onSelectionChange on every internal update and not only
when the selection moved. We answered with a fresh array of ids each
time, React re-rendered, React Flow called back, and the canvas span at
several hundred "Maximum update depth exceeded" a second from the moment
an investigation opened. Nothing looked broken, which is why it lasted:
the canvas was merely slow, and anything asking the main thread for real
work behind it, a report export in particular, waited its turn.

The ids are compared before the state is replaced, so an unchanged
selection now changes nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The mermaid timeline landed in the Markdown only, on the argument that
Markdown is where a diagram can be generated rather than drawn. That
argument was about how to make one, not about who wants one, and the
report image and the PDF are exactly the formats handed to somebody who
will not open a Markdown file.

So the same rows, painted: a rail down the margin, a dot per moment, its
events under it. timelineRows() is now the one grouping the three
renderers share, so a case reads the same whichever file it leaves in.
The PDF draws its rail line by line rather than block by block, so a page
break cuts it where the text is cut instead of leaving it running into
the margin.

The checkbox is no longer tied to the Markdown, and stays optional and
global: a drawing per subject would be several pictures saying what the
by-subject lists already say in less room.

The image report and the PDF also name their parts now, the way the
Markdown does. Both ran the global chronology, the per-subject one and
the undated blocks together with nothing between them, which was
readable while the narrative had one section and stopped being so when
it grew three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
The narrative grew from one section to three and the report image and the
PDF still had two typographic ranks: a small uppercase title, and bold at
text size for everything under it. So "Chronology, by subject" and "The
threat actor Guilde Vermeil" arrived as the same kind of thing, one
naming a part of the report and the other a subject inside it.

Three ranks now, each saying what it is by a different means rather than
by being a point bigger than the one below: a section is small uppercase
in the accent, tracked out and ruled off; a part of it is bold in the
text colour; a subject inside that part is bold and quiet. And more room
between them, which is most of what makes ranks legible at all.

The by-subject section also had a lone dated line reading as the end of
the block above it, with its date sitting in the middle of a sentence:
"The infrastructure Kit AiTM Vermeil 2026-08-10 Consists of the URL...".
The one-off subjects are gathered ahead of the blocks now, and each of
those lines opens with its moment like every other line of the section,
naming its subject in the sentence rather than before the date. In all
three formats, since the ordering comes from timelines().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Three things, all of them about the page.

The graph had a page and used two thirds of it: a graph is never the
shape of an A4, and the third left over was white, so a dark picture
arrived stamped on a white sheet. The page is painted in the canvas
background before anything else is drawn on it, so the leftover is the
colour of the picture and the eye reads one page. The graph is still
scaled to fit whole rather than cropped to fill, because a cropped graph
is a wrong graph.

The prose ran the width of a landscape A4, about a hundred and thirty
characters to the line. The graph page keeps the orientation that suits
the graph; every page of prose after it is portrait whichever way that
one went, which needed the page breaks to name their format rather than
inherit the document's.

And a section starts its own page. A title landing three lines above the
fold is most of what makes a document look thrown together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
Four knowledge bases on one canvas, a case that can run from a phishing
message to a wire transfer to a bridge, a narrative that reads it in the
order it happened, and a report laid out to be handed to somebody. The
answer to "what is this for" changed, which is what the major number is
for here.

Nothing breaks: bundles stay valid, canvases written by 1.x open exactly
as they did, the file format has not moved. Said in the release notes
too, because a 2.0 makes people ask.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZKZRDMVAdrWUQP2crYVXH
@pakdekro
pakdekro merged commit ef639f6 into main Aug 30, 2026
3 checks passed
@pakdekro
pakdekro deleted the feat/f3-fraud-framework branch August 30, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant