Skip to content

feat: basilisk-fang erase — stab the diary and it bleeds out#16

Open
vnnkl wants to merge 9 commits into
MaximeRivest:mainfrom
vnnkl:feat/basilisk-fang
Open

feat: basilisk-fang erase — stab the diary and it bleeds out#16
vnnkl wants to merge 9 commits into
MaximeRivest:mainfrom
vnnkl:feat/basilisk-fang

Conversation

@vnnkl

@vnnkl vnnkl commented Jul 7, 2026

Copy link
Copy Markdown

The diary now has an in-fiction way to forget, and it's the movie's: stab it. v0.3.0's only erase is RIDDLE_MEMORY=off or deleting files over SSH.

The gesture

Hold the pen's eraser still on one spot (drift < 20px). After ~0.8s ink starts bleeding out from under the tip — the warning, and the window to abort (lift, and the page reabsorbs it). At 3 seconds the same bleed accelerates until the page drowns in ink, drains away, and every remembered page dies (MemoryStore::forget_all: entries, index.tsv, *.strokes). While bleeding, the tip stops erasing — it's a fang, not an eraser — so no stroke points are dropped under the stain.

The bleed (one continuous process, no stages)

A pixel is inked once dist(px, tip) ≤ R·f(px), where f is a 3-octave value-noise field anchored to the paper (built once per stab, integer math per pixel over the growth annulus). Only R changes: it creeps during the hold and eases into a rush after 3s. Because the field belongs to the page, growth constantly reveals new structure — fingers run where the paper drinks fast, bays lag — so it reads as spreading ink, never as a shape being resized. Spatter droplets fling ahead as R passes their thresholds and are swallowed later.

(This is the standard ink-bleed construction — threshold of a noise-displaced radial field, the same recipe artists use manually as blur → displace → threshold — after several iterations of user-testing rejected polar-wobble scaling, cellular automata, and compound blob budding as visibly artificial.)

Tested

Extensively on a Paper Pro (Ferrari, OS 3.27.3.0, takeover): pooling, abort-reabsorb, drift-while-erasing, full erase (memory dir emptied, next conversation is a first meeting), and repeated stabs. Documented in oracle.env.example.

🤖 Generated with Claude Code

vnnkl added 8 commits July 7, 2026 20:50
The diary now has an in-fiction way to forget, and it's the movie's:
stab it. Hold the pen's eraser still on one spot; after a second an
irregular ink splat pools at the tip (the warning — lift to abort and
the page reabsorbs it), and at three seconds the splat bursts, ink
floods the page from the wound, drains away, and every remembered page
dies (MemoryStore::forget_all: entries, index.tsv, *.strokes).

The splat is an organic stain, not a circle: its edge wobbles with two
seeded sine lobes so it keeps its shape while growing, ringed by
satellite droplets that land at fixed spots as the splat grows past
each one's threshold, every third flung outward as a radial streak.

While the warning ink pools the tip stops erasing — it's a fang, not
an eraser — so no stroke points are dropped under the splat.
The pooling splat and impact burst were organic, but the page-covering
flood still expanded as plain discs — circular wavefronts painting over
the stain. The flood wavefront now wobbles with the same seeded lobes
(row-span fill, two trig evaluations per row, so a page-sized flood
stays cheap), and the final radius overshoots the corner distance to
cover the wobble's inward troughs.
The trigger cut from the 110px pooled splat straight to a 260px one,
droplet thresholds stopped at ~110 so the growth went bald, and the
flood's slow lobes smoothed into near-circles at page scale — the
seam the eye caught.

Now the pooled splat surges frame-by-frame (same seed, 110→300) with
droplet thresholds extended so fresh spatter lands throughout, and the
flood wavefront adds two higher-frequency lobes so ink reaches out in
fingers all the way to the edges.
Scaling the pooled splat read as a zoom — the same outline resized,
with no new ink flowing — and the wavefront still went geometric by
the page edges.

Now the pooled splat is left exactly as inked and its pixels seed a
cellular automaton in the Suibokuga ink-model tradition: the page is a
6px-cell grid with absorbency from smooth value noise, and ink spreads
to neighboring cells with probability set by absorbency and by how
much ink already borders them. Fingers race down high-absorbency
fibres, bays lag, the front is grainy (jittered discs, never a
lattice), and generations per frame accelerate until the page drowns.
Droplet thresholds return to the original pooling range.
The CA soak read as a texture switch at the 3-second mark: grainy
cell-growth against the smooth pooled splat.

The death now speaks exactly the warning's language: wobbly-edged
lobes (row-span blobs, monotonic in r for a fixed seed) erupt from the
stain's rim, grow in place, and bud outward-biased children — each
announced by the same flung droplet spatter the pooling phase taught
the eye — cascading generation by generation until every corner
drowns. Growth is always new ink arriving, never a resize or a
re-texture of what's already inked.
Every previous design had two visual acts (pool, then death) and the
seam between them always showed. Now there is a single process from
first contact to black page: a noise-displaced distance field — a
pixel is inked once dist(px, tip) ≤ R·f(px), with f a multi-octave
value-noise field ANCHORED TO THE PAPER. R creeps while the fang is
held (the warning pool), and the same R accelerates past 3s (the
death). Because the field belongs to the page, growth constantly
reveals new structure — fingers run where the paper drinks fast, bays
lag — never a resize, never a texture switch. Spatter droplets fling
ahead as R passes their thresholds and are swallowed later.

This is the community-standard ink-bleed construction (threshold of a
noise-displaced radial field — the same recipe artists use manually:
blur, displace, threshold), and the whole thing is integer math per
pixel over the growth annulus, cheap enough for the A53.
The bleed jumped from ~50px/s creep to ~550px/s the instant 3s hit.
The growth multiplier and increment now ramp over the first ~8 frames
(~0.7s), so the creep visibly leans into the rush instead of snapping.
Gentler ramp (0.025/frame, cap 1.22x) and smaller increments; the
page now drowns in roughly three seconds instead of under two.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d736100e92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread riddle/src/main.rs Outdated
Comment on lines +510 to +512
if let Some(bl) = bleed.take() {
if !bl.bbox.is_empty() {
absorb_region(&mut surf, &disp, bl.bbox);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve existing ink when aborting a bleed

When an aborted stab overlaps handwriting, this path dissolves the entire bleed bbox, which removes any pre-existing dark pixels under the warning pool even though erase_point was stopped while bleed.is_some(). That leaves the visible page changed by an action the user aborted, and the stroke model can still retain points for ink that was just whitened, so later help detection or remembered-page replay can use strokes that are no longer visible. This happens after holding the eraser long enough for pooling and then lifting or leaving Listening; restore the underlying pixels or only remove pixels that belong to the bleed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0aa7b5c, via the 'only remove pixels that belong to the bleed' route: bleed_grow records exactly the pixels it turns white→black (pooling only; the death path drops the log), and the abort restores exactly those. Handwriting under the pool was already dark, never enters the list, and survives untouched — screen and stroke model stay in agreement, so no phantom '?' detection or invisible ink in snapshots/memory replay.

Comment thread riddle/src/memory.rs Outdated
Comment on lines +114 to +117
for e in &self.entries {
let _ = std::fs::remove_file(self.strokes_path(e.id));
}
let _ = std::fs::remove_file(self.index_path());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove orphan stroke files when forgetting

This only removes stroke files for entries loaded from the current index, so any <id>.strokes file not represented in entries is left behind after the new “forget everything” gesture. That can happen if index.tsv is missing/corrupt or if append wrote the strokes file and then failed before appending the index, leaving local handwriting data in RIDDLE_MEMORY_DIR despite the erase operation. To satisfy the erase semantics, delete all *.strokes files in the memory directory along with the index.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0aa7b5c. forget_all now sweeps every *.strokes file in the memory dir via read_dir instead of iterating loaded entries, so orphans from a lost/corrupt index or a strokes-written-index-failed append are erased too.

…phans

- Aborting a stab dissolved every dark pixel in the bleed's bbox,
  destroying handwriting under the warning pool even though the action
  was aborted — and leaving the stroke model referencing ink that was
  no longer visible (phantom '?' detection, invisible ink in memory
  replay and oracle snapshots). The bleed now records exactly the
  pixels IT blackened (white→black transitions, pooling only; the
  death drops the log) and an abort restores exactly those, so ink
  that predated the pool survives untouched and screen and stroke
  model stay in agreement.
- forget_all only deleted stroke files listed in the loaded index,
  leaving orphaned <id>.strokes behind after a lost/corrupt index.tsv
  or a strokes-written-index-failed append. It now sweeps every
  *.strokes file in the memory dir — erase means erase.
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