30 Commits
Author SHA1 Message Date
slaguru666andClaude Opus 5 d47a167fd0 Packs go to schema 2, so the catalog stamp records the new roster field
beat.mjs embeds a pack's roster verbatim into every area, so adding
beforeInitiative changed the shape of the emitted delve file — and two files
from the same seed could carry different structures under identical stamps.

The change originates in the packs, not in the generator's composition, so it
belongs to catalogVersion rather than GENERATOR_VERSION: the packs are at
schema 2 and a file now stamps barrow@2. The note on GENERATOR_VERSION says
which stamp owns which kind of change, since that was the ambiguity.

README's test counts were still 39 and silent about the adapter suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.6.5
2026-08-06 00:52:08 +01:00
slaguru666andClaude Opus 5 c1e467c91b Prose wins over the field; the play sheet carries avoid; the guard is testable
Still unreleased, but module.json is at 0.6.5 now.

initiativeWarning checked the field before the prose, so a hand-edited file
carrying both printed the warning twice — the exact fault the migration was
meant to end. Prose is checked first now and always wins.

The play sheet was the only surface that did not render `avoid`, so three
legacy rosters whose warning lived there — church, forest and temple at
nightmare — lost it entirely: the helper suppressed its fallback on finding
prose the play sheet never showed. It renders `avoid` now, which it should
have anyway; it is how the table gets past a fight without having one, which
is table-facing by definition. All five surfaces now render both prose
fields, so "the prose already says it" means the same thing everywhere.

Replayed all 64 rosters in their pre-field form through every surface: 26
need a warning, 130 of 130 checks say it exactly once. The current packs
give the same result.

commit() no longer mutates st before the write — it passes an incremented
copy, so a failed write really does leave the state as it was, which is what
the retry warning promises.

The single-flight guard moved into stage.mjs as singleFlight() and has four
tests: overlapping calls never run together, the second is refused rather
than queued, the guard clears afterwards, and a throw does not wedge it. It
closes the window within one client only; two GM browsers still race, and
that needs a world-level lock rather than a module variable.

41 core tests, 60 adapter tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:46:43 +01:00
slaguru666andClaude Opus 5 1b2f9aa385 The warning reaches every surface; commit and concurrency are covered
Still unreleased. Answering the three blockers.

The worksheet and play sheet never rendered the new field, and stripping
the prose meant they said nothing at all — the two surfaces the whole
two-surface design is about. All five renderers now go through one helper,
core/roster.mjs, which also recovers the warning for delve files generated
before the field existed and refuses to add a line the prose already
carries. Measured across four themes: 40 of 40 delves carrying a roster now
warn on worksheet, play sheet and markdown alike.

Twenty-three was the wrong set. The spelling was never the semantics:
barrow's nightmare Wraith, cave's nightmare Troll and forest's battle Troll
carry the same non-discoverable restriction without the word ONLY. Marked,
so 26. I audited all 64 rosters rather than trusting the count — the ones
left unmarked are discoverable by trying (an Ogre with 12 Grit, mooks that
keep coming until the necromancer stops), and damage works on all of them.

commit() was unprotected, so a failed settings write threw out of stageArea
with the scene already real. It is caught now and says plainly that a retry
will raise the area twice. enter() also has a single-flight guard: it is
bound to a button, it awaits the Forge for seconds, and two overlapping
calls both read the same index.

Also: packById checks the geometry is one VANITY can build rather than just
truthy; foeSection is exported and the suite holds the two renderers to
agreement on all four kinds; and the module README and the seams warning
said 0.10.5 where everything else says 0.10.4.

41 core tests, 54 adapter tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:35:15 +01:00
slaguru666andClaude Opus 5 0bbf57b693 The initiative warning becomes authored data, and staging gets a harness
Unreleased. The warning that v0.6.4 deleted is restored, but not as the
string heuristic that caused both faults — rosters carry beforeInitiative
now, and the renderers print it. 23 rosters have it: the 18 whose harmedBy
restricts what works, plus 5 Troll rosters that carried the instruction in
prose and so had two conventions between them. The prose copies are gone,
so it is said exactly once wherever it is said.

validate-pack enforces both halves — ONLY implies the field, and the phrase
may not appear in prose — and core/test.mjs now runs the validator over all
sixteen packs, so a pack regression fails the suite rather than waiting for
someone to remember the tool. Both rules were confirmed by breaking a pack
deliberately.

enter()'s decisions move to module/stage.mjs behind injected effects, which
makes the failure paths runnable under plain node. That immediately found a
bug in the first version of the extraction: Promise.resolve(fx.stage())
does not catch a synchronous throw, so a Forge that threw rather than
rejected would have taken the whole of enter() down. Nineteen of the new
tests are that matrix — failed scene, failed population with and without a
roster, failed hoard, failed cards, async rejection, and a Forge that
returns nothing.

The contract those tests hold: the scene is the commit point. Fail there
and nothing was entered, so a retry is safe; succeed and the turn advances
before anything else can fail, so a retry cannot restage. Everything after
degrades with a warning that says what was actually lost — the encounter
failure no longer promises a roster the area does not have, and the hoard
failure is no longer swallowed to the console.

Also: game.delve.loadPack is the validating resolver, so raiseDungeon can
no longer stage from a pack with no forgeStageType.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:21:10 +01:00
slaguru666andClaude Opus 5 9d220a3428 v0.6.4 — one classifier for both surfaces, and tests for it
The foe block broke twice in two releases, both times because each
surface decided for itself, in its own hand-written ternary, which of
the cases it was in. classifyFoes() in the new module/foes.mjs makes that
choice once — forged, planned, unavailable, none — and both surfaces
switch on it. foes.mjs is Foundry-free on purpose, so foundry-module/
test.mjs can run it under plain node with no shim. Fourteen tests, each
one a case that shipped broken or nearly did.

enter() now survives a Forge that throws. It had no catch, so a failure
left the scene raised, no cards posted and the turn not advanced, and a
retry would stage the area twice — while the comment above it claimed
the branch handled exactly that. A failed stage stops before the turn
advances, since there is nothing to run; a failed population keeps going,
because the scene is up and the planned roster stands in.

An area with combat heat but neither actors nor a roster used to render
nothing at all. It now says so and points at the decision and fallback.

The "say so before initiative" addendum is gone from all three renderers.
It was gated on the literal word ONLY, and the palace, port and village
battle rosters already end with that exact phrase, so it printed twice.
The packs that need the instruction carry it themselves.

Also: draft() spreads params before the pack, like raiseDungeon; and
packById validates the cached pack rather than trusting it, so the
fail-closed rule holds at boot too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.6.4
2026-08-06 00:05:26 +01:00
slaguru666andClaude Opus 5 3a180479cb v0.6.3 — the plan stays runnable when nothing is forged
0.6.2 fixed the foe block by showing the actors the Forge created, and in
doing so broke the populate-off path: with nothing forged there were no
combat numbers at all, and the caption still pointed the GM at "the table
above" when no table had rendered. Turning population off used to leave
the planned roster runnable. It does again.

One rule now governs both surfaces: show what exists. The roster's
tactical guidance describes the planned foes, so it travels with the plan
and only when the plan IS the encounter. With foes forged, the plan is a
one-line note saying its tactics do not describe them; with nothing
forged, the full planned roster renders and its guidance applies, because
there it is the encounter. The previous wording claimed the guidance
applied either way, which asserted exactly what the fix existed to deny,
and gating the caveat on the literal word ONLY missed the Troll that
regenerates unless burned, the Ogre's 12 Grit and the Skeletons that
return until their Necromancer stops.

packFor no longer falls back to barrow. Generated files always record a
theme, but load() takes hand-edited JSON too, and guessing the geometry
is the bug it was written to prevent — it now fails closed, and also
rejects a pack with no forgeStageType. draft() keeps a default because
drafting chooses a theme rather than being told one.

raiseDungeon spreads caller params before the pack, so a programmatic
raise({pack}) can no longer generate from one pack while the maps are
staged from another.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.6.3
2026-08-05 23:53:13 +01:00
slaguru666andClaude Opus 5 7f5804e2f1 v0.6.2 — publish the theme and roster fixes
Both are correctness bugs a GM would hit on the first non-barrow delve,
so they should not wait for the next feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.6.2
2026-08-05 23:42:34 +01:00
slaguru666andClaude Opus 5 1ca0a64295 Stage the delve's own theme; show the foes that exist, not the plan
Two bugs, both of them the adapter drifting from what the core says.

PACK was a module global set once at boot to barrow. load() never
switched it and enter() read PACK.forgeStageType, so a port delve
authored at the desk staged barrow geometry — the fiction said quayside
and the map was a burial chamber. Fifteen of the sixteen themes, silently
wrong, on the path the module documents as its intended entry point. It
now resolves per call from d.params.theme, which is written from pack.id
at generation and so is always present and always right. Per call rather
than once at load, because a page reload resets the global while the
staged delve in world state survives; and before the folder is created,
so a bad theme fails without leaving anything behind.

The pack roster describes the encounter DELVE planned. The Forge takes no
cast — it rolls its own monsters from the heat — so the roster and the
actors in the world were never the same list, and both surfaces printed
the roster's stats as though they were. In several themes the roster also
carries "blessed, silvered or magical weapons ONLY", naming a Wraith that
was never created; the journal told the GM to say so before initiative.
Both surfaces now run off the actors that exist, read from the documents
themselves, and the plan is kept but labelled as the plan. The chat card
drops the immunity line entirely — it is the live surface and a false
immunity is worst there; the journal keeps it captioned, since a GM at
the desk may choose to cast the fight by hand.

Adds one core test for the contract the fix rests on: every delve records
the pack it came from, across all sixteen. The adapter itself has no test
harness — see the note in the commit for the release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:41:31 +01:00
slaguru666andClaude Opus 5 316716e001 Bump GENERATOR_VERSION to 0.2.0; let the tests run from anywhere
GENERATOR_VERSION had sat at 0.1.0 while the emitted object gained the
authored layer and the content model moved under it, so a delve file
claimed a generator that no longer existed — both samples say 0.1.0 and
only one of them has an authored layer. Nothing gates on the stamp, so
this is informational only and old files still load. The comment on it
now says what it tracks and when to bump it, which is what stopped it
being bumped before.

test.mjs read ./content/barrow.json relative to the working directory, so
it only ran from inside core/ and crashed from the repo root. It now
resolves from the module file, matching build-index.mjs and
validate-pack.mjs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:58:29 +01:00
slaguru666andClaude Opus 5 246a5f607d v0.6.1 — publish the corrected VANITY floor
The 0.10.4 requirement only reaches an installing GM through the release
manifest, so it needs a release to take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.6.1
2026-08-05 22:54:18 +01:00
slaguru666andClaude Opus 5 5fae50e25b Correct the VANITY floor to 0.10.4; bring DESIGN.md up to draft 7
The manifest asked for VANITY 0.10.0, but the Forge seams only landed in
0.10.4. Below that floor the seams are absent and every delve emits the 9
chat cards and 3 folders the spike measured — the exact failure the seams
exist to prevent. The README said 0.10.5, which was safe but wrong; both
now say 0.10.4 and say why.

DESIGN.md still described the tool as an unreleased core with one theme.
It now matches what shipped: seams merged and released, the Foundry slice
at v0.6.0, 16 themes across 5 geometries, 39 tests, the pack validator and
the authoring layer. The features pool is motif-scoped now, so the "last
piece of generic furniture" is gone.

Open questions gain the one that matters: DELVE has never been run at a
live table, and no amount of desk validation will close that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:29:21 +01:00
slaguru666andClaude Opus 5 7af690205c feat: wild family complete — cave, mountain, river, lake, valley theme packs
Five wild-kernel themes, each 4 motifs:
  cave      dark · deep · echo · keeping        (cave geometry)
  mountain  ascent · cold · pass · weight       (cave)
  river     crossing · current · course · drowned  (fen)
  lake      stillness · mirror · sunken · rising   (fen)
  valley    ridges · shelter · bottom · weather (forest)

Two rendering fixes surfaced by the new claimants, both in skeleton.mjs:
  - `{conscript}'s` rendered "the climbers's"; conscripts are plural noun
    phrases, so fill() now collapses s's to s'.
  - claimants carrying their own article rendered "The The Grey"; named()
    only prefixes when one is absent.

All 16 themes validate; 39 tests pass. Module → 0.6.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.6.0
2026-08-05 19:22:01 +01:00
slaguru666andClaude Opus 5 d22421efbd feat: settlement family complete — village, market, city, port theme packs
Four settlement-kernel themes on village geometry, each 4 motifs:
  village  kinship · harvest · custom · quiet
  market   bargain · weights · crowd · credit
  city     works · rolls · wards · liberties
  port     cargo · passage · tide · wreck

All validate. 11 themes total; index.json rebuilt. Module → 0.5.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.5.0
2026-08-05 18:57:31 +01:00
slaguru666andClaude Opus 5 b4990533a8 feat: interior family complete — church, temple, palace theme packs
Three interior-kernel themes on barrow geometry, each 4 motifs:
  church   faith · congregation · offering · sanctity
  temple   exactness · sacrifice · silence · exclusion
  palace   service · delight · splendour · audience

All validate. 7 themes total; index.json rebuilt. Module → 0.4.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.4.0
2026-08-05 17:48:28 +01:00
tevansandClaude Opus 5 075be5c1de Town and Forest: two new kernels, not just two new packs
Barrow and Castle share a kernel — a dead claimant whose appetite outlived them.
A market or a river has no corpse in it, so the other two families needed the
kernel to generalise, and it does: something wanted a thing, and something was
done to keep it that way. Only who "something" is changes.

TOWN, on village geometry. The appetite belongs to the place and the claimants
are offices — a Corporation, a Watch Committee, a Guild of Mercers. It wants to
be prosperous, respectable, safe or necessary, and it priced people out,
chartered them away or struck them from the rolls to stay that way. Motifs:
ledger, appearances, vigilance, thoroughfare.

FOREST, on forest geometry. The appetite belongs to the wood, and the claimants
are its own names for itself. It wants to be entered, fed, spread or left
uncounted, and it took people, grew through them, or let them wander. Motifs:
paths, tithe, reclaiming, trackless.

Settlements also needed their own naming rule. A barrow is named for its
occupant; a town is not — it has a name and the claimant merely runs it. Packs
declare placeNameStyle, so "The Town of Watch Committee" is now Cold Harrow, run
by the Watch Committee.

Three of fifteen requested themes are done. The remaining twelve are the same
job repeated rather than a design question, since all three kernels now exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.3.0
2026-08-05 16:41:10 +01:00
tevansandClaude Opus 5 e2d1de2fb8 Themes are pluggable; add Castle; validate packs
Groundwork for many themes, plus the first new one.

A pack validator enforces every structural rule this project learned the hard
way: situations that render as broken English, decisions that gate progress with
no alternative, motifs too thin to fill six areas without repeating, rosters
that do not say what harms them. Fifteen packs cannot be hand-checked; this
checks them in a second.

Themes are now discovered from content/index.json rather than hardcoded, so the
CLI takes --theme and --themes, the Foundry dropdown fills itself, and packs
load on demand instead of all at once.

Castle: four appetites — to be feared, never to give ground, to be obeyed at
once, the name to go on — over barrow geometry, since a keep is rooms and
corridors. 168 kernels from its own claimants and accommodations.

Fixed a bug the second theme exposed: the question the bottom problem asks was
hardcoded against barrow's appetite ids, so every other theme fell through to a
generic "Well?". It lives on the appetite in the pack now, and the validator
requires it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.2.0
2026-08-05 16:14:33 +01:00
tevansandClaude Opus 5 592d540910 v0.1.3 — fix the relationship schema that blocked manifest install
Installing by manifest URL failed on a remote server:

  [Module.vanity-delve] validation errors: relationships: systems:
  ArrayField 0: type: requires is not a valid choice

The relationship entry used type: "requires". In Foundry's schema `type` is the
PACKAGE type — module, system or world — and the relationship kind is expressed
by which array the entry sits in. So the correct value is "system".

The entry now also carries VANITY's manifest URL, so Foundry can offer to
install the system rather than just refusing.

This was never caught because the module had only ever been installed by copying
files into Data/modules by hand. The validator that rejected it only runs on the
manifest-install path, so every local test passed while the published artifact
could not be installed at all — the same shape as the scene-control hook bug,
where testing the layer below the interface hid a failure in the interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.1.3
2026-08-05 13:55:14 +01:00
tevansandClaude Opus 5 1be441f775 Add MIT licence and a repo README
The repo went public with no LICENSE, which means default copyright — nobody
could legally reuse any of it. MIT throughout, covering the generator and the
VANITY prose alike.

Also adds a README, since the front page was previously a design document that
opened mid-argument, and sets module.json's license field so Foundry shows it.

Module to 0.1.2 so the released zip carries LICENSE too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.1.2
2026-08-05 12:55:33 +01:00
tevansandClaude Opus 5 decb3bf930 Replace the footers; the repo is public now
Eleven files carried "Private VANITY development material — not for sale or
distribution", which contradicted the repo the moment it went public.

Two replacements, because the files do two different jobs. core/render.mjs
*emits* its footer into every generated delve, so that one now says where the
document came from rather than restricting what can be done with it — a
generated dungeon belongs to whoever generated it. The project's own documents
— DESIGN.md, the paper delve, both playtests, the spike report — read as
development notes in a public repo.

Samples regenerated so they carry the new line. The worksheet and play sheet
never had a footer and still do not; they are the GM's own working documents.

Module to 0.1.1: the v0.1.0 zip vendors the old render.mjs and would stamp the
stale footer on everything it generated. Cutting a version rather than swapping
assets under the existing one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.1.1
2026-08-05 12:38:32 +01:00
tevansandClaude Opus 5 9d953dba94 Make the module installable: vendor core, point at release assets
What was on git could not be installed. foundry-module/ referenced core/ rather
than containing it, so an installed copy had no generator at all — it only ever
worked here because the live module directory had core/ copied in by hand.

core/ is now vendored into foundry-module/module/core/, minus cli.mjs and
test.mjs which are desk tools. module.json points manifest and download at
release assets, the way mapwright distributes, so a remote server can install by
URL without reaching into the repo tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.1.0
2026-08-05 11:18:23 +01:00
tevansandClaude Opus 5 ded836d4c8 Deal area names; fix the scene-control hook for Foundry v13+
Two bugs the first real button-press exposed.

The ⛏ never appeared. getSceneControlButtons was written for the old array
shape, but v13+ passes a record keyed by name and the token group is `tokens`,
plural — so the hook silently added nothing. Every earlier test had gone through
the API, which masked it completely. It now handles both shapes.

Area names were picked per-area from a small per-role list, so two areas sharing
a role could collide: a generated dungeon shipped with two scenes both called
"The Lesser Vault". Names are now dealt globally without replacement, falling
back to other roles' lists before ever repeating. This is the same bug already
fixed for features, decisions, temptations and situations — missed for names
because names were assigned in beat.mjs rather than dealt in delve.mjs.

A test now generates 180 delves at 6, 9 and 12 areas and asserts no two areas
in one delve share a name. Tests 38 -> 39.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:44:53 +01:00
tevansandClaude Opus 5 47778fdbf6 The Forge: one button, a whole dungeon in the world
The brief was a tool in Foundry that generates everything on a click and injects
it as a new dungeon. Previously the module could only stage a delve authored at
the desk, and the only way in was a console command — an API with a button glued
on, which is why it looked like nothing was there.

The ⛏ button now opens a form (theme, areas, depth, heroes, deadliness, density,
greed, clock, ending, seed, and whether to build maps and roll encounters), and
Raise the dungeon builds the lot in about five seconds:

  • a folder per document type, named for the dungeon
  • one scene per area, walls and lighting placed, via the VANITY Forge
  • encounter actors, foldered rather than scattered across the world
  • a JournalEntry — GM truth, a page per area, the ending — which is the
    adventure, and is written to be read cold

The journal is the deliverable. Scenes without it are a pile of maps.

Generating is cheap, so removing had to be. Every document DELVE creates is
stamped with a dungeonId, and a 🗑 button lists what it built and deletes one
dungeon entirely — confirming with exact counts first, and never deleting the
scene the GM is standing in. Colour is not an identifier: it is a Color object,
and a GM may recolour a folder.

Verified live: two dungeons raised from the form and the API, then one removed
cleanly at -3 scenes, -10 actors, -1 journal, -3 folders with nothing else
touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:27:23 +01:00
tevansandClaude Opus 5 cda61e63dc Module stages a finished delve, and splits the audience
DELVE is an authoring system now, so the module's job changed: it no longer
generates at the table, it stages what the GM finished at the desk.

game.delve.loadFile('name') reads a finished working file from the world's
delves/ folder; load() takes an object; draft() still generates in-world but
announces itself as unfinished. outstanding() and ready() report what is
missing, and an unfinished delve says so on load rather than pretending.

The important change is two audiences. The read-aloud is what players are
there for, so it is posted publicly, spoken by the area's name. The truth,
the numbers, the situation and the leverage point stay whispered to the GM.
Everything used to be whispered, which meant the one thing written for the
table never reached it.

Verified live in the-vain-crown: a finished five-area delve loaded, reported
zero outstanding, and staged in 5.8s — six public read-alouds, six GM whispers,
nine actors in one delve folder, zero loose Encounter folders. Test debris
swept afterwards; the world is back to its original 8 scenes and 30 actors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:52:15 +01:00
tevansandClaude Opus 5 df01615c5a Pivot: DELVE is an authoring system
Four readings of generated output reached the same verdict, and the last one
answered 'would you run this tomorrow' with No. Three renderer passes each
fixed what the previous review named and each introduced a new fault: adding
resolution blocks made it menu-like, and subtracting blocks buried the clue and
the skip guidance. The density is not a rendering bug. Everything on the page
is a vice at the table and a virtue at the desk.

So one working file, two surfaces. The worksheet carries everything, labelled,
with reroll commands beside each component and explicit prompts where only a
human can write. The play sheet carries the GM's own prose, the leverage point,
the decision on one line and the numbers — 61 lines against the worksheet's 137.

The JSON is the document. An authored layer holds what the GM writes and the
generator never touches it, so a reroll cannot destroy prose. Lock what is
good, reroll what is not, write the read-aloud, export.

Two bugs found while building it, both in reroll: the exclusion set omitted the
area's own current value so a reroll could hand back what was already there,
and when every option was in use — six areas against a six-item pool — the
fallback did the same. A reroll that changes nothing looks broken.

DELVE does not write the read-aloud and does not write the climax. It gets a GM
to a strong first draft in a minute instead of an evening, then gets out of the
way. Tests 28 -> 38.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:48:17 +01:00
tevansandClaude Opus 5 6517b10111 Fix the situation opener; guard the grammar in tests
A cold read caught the subtraction pass emitting broken English:
'Nobody — but the bells are moving is a summons is being rung...'. The opener
template assumed occupant was a noun phrase and doing was a participle; ten
situations broke that, and one occupant carried its own clause.

Root cause fixed in the data, not the template. The template also drops its
hardcoded copula for an em-dash appositive, which removes subject-verb
agreement as a failure mode entirely — 'The bells is ringing' cannot recur.

Three invariants now guard it: occupants carry no verb, actions are participle
phrases, and no rendered opener contains a stray copula. The first version of
that last test was wrong rather than the output — it counted em-dashes and
flagged 191 false positives on cue fragments that legitimately contain one.
The audit regex also had a false negative, since 'something' ends in -ing.

Tests 25 -> 28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:35:29 +01:00
tevansandClaude Opus 5 2dafb9be33 Subtraction pass: integrate the area block
Third reading said the core criticism was gone — areas now arrive as playable
rooms — but that the result was over-stuffed: 'a room plus its operating
manual'. Asked for the single remaining gap, it said one word: integration.
The hand-written delve makes image, situation, pressure and choice read as one
thing; this presented them as stacked components.

So this removes blocks, not content. The image, the situation and the automatic
clue become one opening paragraph to read from. The decision collapses to
attempt/miss/or. Trigger, bane beat, fallback and skip-guidance become a single
GM footer line. The per-area truth drops the delve-level transgression, which
was already in the header and was being reprinted in every area.

Areas go from roughly thirty lines to eleven to eighteen.

A new invariant asserts the pass removed blocks and not information: for twenty
delves, every situation, onArrival, because, offer, decision, alternative,
trigger, fallback, temptation and roster must still appear in the rendered page.
Tests 24 -> 25.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:29:06 +01:00
tevansandClaude Opus 5 906a034ffe Areas arrive as situations, not prompts
The review's verdict after resolution blocks: 'still choices attached to
prompts, not spaces that naturally produce play'. The hand-written delve gives
a place — four people back-to-back around a dead lantern — and the decisions
fall out of it. DELVE gave a decision and asked the GM to imagine the room.

Every area now carries a situation: an occupant, what they are doing, what
changes when the party walks in, why (GM), and what it offers. 48 across 8
motifs, motif-scoped and dealt without replacement.

It produces chains nothing planned: in seed live-table-01 a grave-robber in
area 2 is trying to get a password's intonation right, and area 4 is a door
that needs one. Three routes to it now exist where there was a Wits roll.

Also closes the last two open complaints: the five roll-less decisions now have
real attempts (a voice you could not speak to was the weakest), and {priorCue}
resolves against a cue the party has actually seen instead of granting the
answer by fiat.

Tests 19 -> 24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:22:29 +01:00
tevansandClaude Opus 5 09dd58bff0 Resolution blocks on every decision; motif-scope the features pool
Playtest 3 put four players (Codex) through a generated delve with only the
player-facing text. Verdict: "a list of room-prompts, not a place", which
"expects the GM to invent too much of the actual game". It localised that to
one thing — decisions had a cue and a cost but no resolution.

Every decision now carries: an attempt roll, an explicit success outcome, an
explicit failure outcome, and an always-available alternative. 48 blocks across
8 motifs. The alternative is not optional: playtest 1 died on a gate the table
could fail without knowing, and the sharpest complaint this time was a door
whose only options were "guess blind and bleed Banes" or "pay time".

Features are now motif-scoped too, so all three furniture pools belong to the
fiction rather than a global list. 136 authored entries per theme.

Three new invariants make the failure modes unauthorable: every decision has a
resolution, no decision gates progress without an alternative, and every rolled
decision states both success and failure. Tests 16 -> 19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:09:24 +01:00
tevansandClaude Opus 5 ee1183e897 Motif-scope the decisions and temptations
The review's sharpest finding was that the fiction cohered but the things in
the rooms did not belong to it: only the motif cue fragment was projected, while
decisions and temptations came from global pools. The output was a consistent
wrapper around generic room furniture.

Both now live under motifs.<id> — 6 decisions and 5 temptations each, 88
authored entries across 8 motifs. A `keeping` delve offers a flower that has not
wilted and a room you may search or leave exactly as you found it; a
`precedence` delve offers a corridor that will not let two walk abreast.

Two bugs surfaced while wiring it, both the same shape as one fixed earlier for
cue fragments: decisions and temptations were picked independently per area
rather than dealt, so a pool of 3 repeated a temptation in 75 of 80 test delves.
Both are now dealt without replacement.

Where a long delve outruns its motif pool, decisions top up from the generic
pool but temptations do not. A generic relic breaks the fiction; a repeated one
only looks thin.

Tests 11 -> 15: motif provenance for both pools, and no repeats at 6 and 9 areas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 07:48:05 +01:00
tevansandClaude Opus 5 07148df8e6 DELVE core: seeded generator, fiction kernel, Barrow pack, CLI
Build steps 1, 2 and 4 of the plan in DESIGN.md.

core/ is pure JS with zero Foundry globals and 11 passing invariants. The
fiction model has one root draw — an APPETITE plus the ACCOMMODATION someone
made for it — and projects the motif, danger, prize, faction, cue fragments and
the appeasement move from it, so the fiction coheres instead of being four
unrelated draws. 336 kernels from 21 authored lines; 60 generated delves gave
51 distinct kernels.

Cue fragments are tagged by facet (institution/ritual/demand/wound/anchor) and
dealt without replacement. The facet progression across the arc reproduces the
hand-authored paper delve's structure, which had arrived at it by instinct.

Generation and table dice stay apart: pressure.mjs decides when a clock roll is
due and what it means, and never rolls. That is what makes the skeleton seed an
honest promise rather than a half-truth.

paper/ holds the hand-written delve and both playtest logs. spike/ holds the
instrumented harness that measured the Forge outside Foundry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:16:58 +01:00