RFC 0015 — the Novel Factory: complete system design and 48-hour build
Status: APPROVED by the author 2026-09-23 (~05:00 UTC). Build in progress; the live build state is the factory itself (prose --order list, prose --factory next), not this file. This document changes only through an engine work order.
This is the blueprint. Its first engine order commits it as
docs/rfc/0015-novel-factory.mdand seeds its build tree into the database (WorkOrders). From then on, no session works from memory or from this file. Every session is driven by the factory's live state in the Hub.Revision 2 folds in an adversarial review that checked every claim against the code and the live database. Each defect it found is marked [RT#n] where it's fixed.
0. Context
Six months and 1,267 commits left a pile of disconnected organs. The author has given a 48-hour deadline, or the project shuts down. They want a factory that writes novels: engineered plumbing where every part leans on the others and keeps itself going. It must exist between sessions, record every change, and have no loopholes, no unrecorded ideas and no vagueness.
What the full history shows:
- What worked:
- Claude writing in-session through the Hub, then reading it in full and fixing it with exact hand splices. That shipped VIGL, TFAH, RESIST, TRUCE and the Gospels, and produced about 427 kept BCODA fixes.
- Checks the engine computes from its own state: TextHash, temporal history, and tonight's read gate.
- What failed: everything claimed instead of computed.
- 43 instruments, which produced 8 applied findings out of 30,745.
- Parallel copies of the story.
- Services that were never wired in.
- Autonomous rewriters.
- Plans that no session loaded.
The design principle: every fact the factory relies on is either stored truth (prose, world, read receipts, decisions) or computed from it. Nothing is maintained by hand. Nothing counts because Claude says so. Every change is a logged Hub call or a commit tied to an approved work order.
Not splitting the book. The scale problem is work that outlives a session, and it's solved by state in the database. A chapter is a unit of work. Reading happens in one continuous pass in order, so a chapter is never a boundary of sight.
No new technology. No neural net, no graph database, no schemaless store. SQL Server, EF Core, the Hub and the MCP server stay as they are.
0.5 State baseline: what works right now, measured before anything is proposed
Measured 2026-09-23 around 04:50 UTC with read-only checks. Every feature in this RFC must close a gap in this table (see the traceability table in §0.6). Anything that closes no gap is out of scope.
| # | Component | State | Evidence |
|---|---|---|---|
| B1 | Hub | working, build cc1e9a687e76, pid 60496 |
GET /api/health returned status=ok |
| B2 | Deployed code vs source | the live Hub is the pre-removal tree plus the read gate and splice (deployed 22:50). The source tree is ahead of it and partly broken | Hub.exe mtime 22:50; the stage-1 agent reports Core, Cli and Mcp build, while Hub, WriterUi, ObserverUi and UnitTests don't (unverified, re-measure in I0) |
| B3 | Git | master at cfaabaa64. Nothing from tonight is committed: 149 tracked files modified or deleted (57 deleted), 31 untracked |
git status --porcelain |
| B4 | Command ledger | working and recording every Hub call. 40 calls in the last 2 hours, all present. Actor is null in 40 of 40. No book column | command_log |
| B5 | Prose read | working. BCODA has 521 beats (the recursive count) | read_beats: total 521 |
| B6 | Prose write (splice) | working through the CLI; the dry-run guard was verified live | earlier session runs; 13 BeatSpliceTests |
| B7 | Read gate | working, no override. BCODA 521/521 unread, export refused | a live --export-node refusal; 12 ReadGateTests |
| B8 | Export pipeline | working before the gate; V72 produced at 21:34 | the export folder listing |
| B9 | Entity write | partial. create_character and create_weapon work with read-back (5 records fixed tonight). Behaviour, timeline, cyberware, neural and ancestry fields can't be written. location is silently dropped. Lists split on commas |
tonight's read-backs; the audit agent |
| B10 | Entity truth | wrong in places. Kyle has a mother, severed hands, piezo and Seo as mentor; Pixel has a hand-reattachment hook; War Dog describes another book; age 0 across the cast; random ancestry. BCODA's cast (≥3 tags) is 76 entities across 11 types | get_character samples; review queries |
| B11 | Mentions | incomplete and asynchronous. 50 of 476 top-level beats have no tags. Mentions are written by a fire-and-forget task | review queries; NodeWorkbenchService.cs:453/641 |
| B12 | Change signal | noisy. CharacterRepository.Save bumps ModifiedAt even on a no-op save and rewrites every bridge table |
Repositories.cs:217 |
| B13 | Findings | dead weight: 15,823 new, 14,914 dismissed, 1 triaged, 8 applied | findings_stats |
| B14 | Session start | injects stale instructions. SessionStart runs inject-digest.ps1 (37.6KB, dated 09-04, mandates ledgers, cites a non-existent AMENDMENTS.md), plus build-prose-mcp.ps1 and start-prose-hub.ps1. UserPromptSubmit runs quickload-on-do.ps1. PostToolUse runs a WorldValidation test hook for engine/data edits. There is no Stop hook |
.claude/settings.json |
| B15 | Session continuity | 12 archived quicksave.md.00N files and agent-queue.json.001; no live quicksave |
.prose/ listing |
| B16 | Claude memory | 662 files, 3.2MB; MEMORY.md is 17.7KB (limit about 24KB) | directory listing |
| B17 | MCP server | stale schema. Tonight's tools (splice_beats, read_status, read notes, read_beats markRead) aren't visible until Claude Code restarts; the CLI twins work now |
the session's tool list |
| B18 | TTS / audiobook | not working. No Kokoro module and no piper.exe; the last MP3s are from June (ElevenLabs). The audiobook export bypasses the read gate |
the review's checks; NodeWorkbenchService.cs ≈2995 |
| B19 | Unit tests | unknown now. 2,837 passed before the removal edits; not run since | last run 21:57 |
| B20 | Autonomous writer | exists and has been unused since RFC 0009. Known defects: sceneSoFar resets every chapter and is capped at 6KB; its facts come from ledgers; RelationshipContext is never set |
the audit agent |
| B21 | Writer UI, KdpPublish | unknown. Not exercised since 09-20 and 08-11 respectively | none (measure only when needed) |
| B22 | Book content | good prose, but not read in order since tonight's fixes. A spot check found a real candidate defect: beat #19900 says the katana is "forty years older than he was", which conflicts with the late-2100s Seo date | read_beats #19900 |
0.6 Traceability: every feature closes a measured gap
| Feature (§) | Gaps closed |
|---|---|
| I0 baseline re-measure | B2, B19, B18, B21 (turn "unknown" into measured) |
| WIP backup plus finishing stage 1 (I0–I1) | B3, B2 |
FactorySessions, SessionStart/Stop hooks (§6) |
B14, B15 |
WorkOrders (§2.2) |
B15 (queue), plans that live in md files, B3 (every commit tied to an order) |
| Actor stamping plus journal from temporal history (§3.13) | B4 |
Rulings (§2.2, §3.6) |
rulings living only in B16 memory |
CharacterFieldWriter, the location error (§3.4) |
B9 |
| Save no-op fix (§3.5) | B12 |
EntityVerifications / F1 (§3.3) |
B10 (proves records were checked) |
Tag-derived mentions in the gate; CaptureScanner part (b) (§3.2, §3.8) |
B11 |
CaptureScanner part (a) (§3.8) |
new names never captured |
| Audiobook gate; TTS smoke test; audio station (§3.2, I0) | B18 |
Exports / F7 (§2.2) |
the version number was never tied to content (B8) |
ContextBundleService (§3.9) |
B20 (in-session writing without the reset) |
MetricsReport (§3.7) |
measured LLM tics |
| MEMORY.md index rewrite (§6) | B16 |
| Not built (no measured gap): new UI, new instruments, graph database, embeddings, autonomous-writer changes, Findings cleanup | B13 stays dead weight, dropped with the PENDING DROP tables after GO |
1. System architecture
┌──────────────── AUTHOR ────────────────┐
│ rulings · approves engine roots · │
│ listens to audio · optional notes │
└──────▲─────────────────────────┬───────┘
│ MP3 (if TTS passes I0) │ words
┌──────────────────────────────┴───┐ ┌────────────▼──────────────────────────────┐
│ CLAUDE CODE SESSION (the worker) │ │ HOOKS (.claude/settings.json) │
│ writes, reads, fixes, corrects │◄──ctx──┤ SessionStart factory-start.ps1 (HTTP) │
│ records, records decisions — │◄──1ln──┤ UserPrompt factory-reminder.ps1 (HTTP) │
│ ONLY through the tools in §5 │──stop─►│ Stop factory-guard.ps1 (after I1)│
└───────┬───────────────┬──────────┘ │ PostToolUse memory-nudge.ps1 │
│MCP │CLI (prose.cmd) └──────────────────────┬────────────────────┘
┌───────▼───────┐ ┌─────▼────────┐ │ HTTP 127.0.0.1:5900
│ Prose.Mcp → │ │ Prose.Cli → │ forwarders only; never touch │ /api/factory/*
│ HubInvoker │ │ HubCliClient │ the DB │
└───────┬───────┘ └─────┬────────┘ │
┌───────▼───────────────▼──────────────────────────────────────────▼───────────────────┐
│ PROSE.HUB — only process that reaches the DB; every call → CommandLedgerEntries │
│ (Actor now stamped: source + factory session id) │
│ FactoryService: stations · status matrix · next action · journal · usage check │
│ ├ ReadGateService (F5, export gate) ├ RulingService (F6 zero-tolerance law) │
│ ├ EntityVerificationService (F1, last) ├ MetricsReport (book-level tics) │
│ ├ CaptureScanner (F4) ├ ExportRecorder (F7) │
│ ├ ContextBundleService (working memory) ├ WorkOrderService (engine/author) │
│ ├ CharacterFieldWriter (full write path) └ FactorySessionService │
│ └ NodeWorkbenchService / BeatSpliceService — the one prose door │
└───────────────┬──────────────────────────────────────────────────────────────────────┘
┌───────────────▼──────────────────────────────────────────────────────────────────────┐
│ SQL SERVER prose: Nodes·Beats·BeatNodes (temporal) world: Entities·Characters+ │
│ bridges·CharacterReadModels·Places·… senses: BeatReadReceipts·BeatReadNotes │
│ law: Rulings factory: EntityVerifications·WorkOrders·FactorySessions·Exports │
│ record: CommandLedgerEntries · *_History temporal tables · ArchivedBooks │
└──────────────────────────────────────────────────────────────────────────────────────┘
Git repo: code only; every commit names a work order. Exports: ePub\MindAttic\<U>\<CODE>\
2. Data model
2.1 Existing tables, reused
| Table | Role | Change |
|---|---|---|
Nodes, BeatNodes, Beats (temporal) |
the prose; its order is the timeline | none |
Entities (ModifiedAt), Characters + bridges, CharacterReadModels, Places, … |
the world | [RT#1] saves stop bumping ModifiedAt when nothing changed (§3.5) |
BeatReadReceipts, BeatReadNotes |
the senses | none |
CanonDocumentSections |
universe laws | none |
CommandLedgerEntries |
every Hub call | [RT#12] Actor is stamped with the source plus the factory session id |
*_History temporal tables |
row-level change history | read by the journal ([RT#12]) |
BeatEntityMentions is no longer relied on by the factory or the gate ([RT#3]). Mentions are derived on the spot from the beat text (§3.2).
2.2 New tables (one additive migration, AddNovelFactory)
Rulings: law, not facts ([RT#16]: there is no EntityId, so entity facts live only on entity records).
| Column | Type | Notes |
|---|---|---|
| Id | guid PK | |
| UniverseId | guid | |
| BookId | guid? | null means universe-wide |
| Kind | nvarchar(16) | law, metric or incidental |
| Text | nvarchar(2000) | the author's words, verbatim |
| Pattern | nvarchar(400)? | .NET regex, IgnoreCase, matched on tag-stripped text |
| MaxPer1kWords | decimal(9,3)? | metric only |
| Source | nvarchar(64) | author or session:<id> |
| At | datetime2 | |
| SupersededById | guid? |
- Kinds:
law: a constraint with a zero-tolerance Pattern (e.g.AGREEDinside the Coda region,piezo, a mother for Kyle), or pattern-less text shown to the writer. Examples of pattern-less text: the Observation Rule, and "Kyle never learns the entity is himself".metric: a book-level tic target.incidental: a proper name that intentionally has no entity.
- Invariants: the Pattern must compile, or the insert is refused. A
metricneeds both Pattern and MaxPer1kWords. A superseded row is inert. - Reuse of
ProseLessonswas checked and rejected: it has no pattern and no scope, and it holds LLM-derived craft lessons with different meaning.
EntityVerifications: "this record was examined against this book".
- Columns: composite PK (EntityId, BookId), RecordModifiedAt datetime2, MentionsFingerprint char(64), VerifiedAt, By, SessionId.
- [RT#2] A record's version is simply
Entities.ModifiedAt, the same signal the read gate already trusts. There are no per-type serializers. It's correct once §3.5 stops no-op saves from bumping it.
WorkOrders: the build tree and author requests. Book work is computed, never stored.
- Columns: Id, ParentId?, RootApprovedBy? (the author's approval, on roots only), Kind (
engineorauthor), Title, Detail, PathsJson, ChecksJson, Blocking bit, Status (open,closedorabandoned), OpenedAt, OpenedInSessionId, ClosedAt?, EvidenceJson, CommitHash?. - [RT#13] Gaming closed:
- An engine order must descend from a root the author approved. The seeded RFC tree counts as approved when this plan is approved. New roots need the author.
- It closes only when the Hub has validated every check itself.
FactorySessions: sessions as rows. These replace .prose/quicksave.md* and .prose/agent-queue.json.
- Columns: Id, ClaudeSessionId, StartedAt, EndedAt?, GitHeadStart, GitHeadEnd?, StartActionJson, EndSummaryJson (
{done[], decisions[{text, rulingId|orderId}], next}). - Invariant:
session_endis refused while any decision lacks an existing Ruling or WorkOrder id.
Exports: proof of what was pressed.
- Columns: Id, BookId, Format (
docx,epub,pdf,txtormp3), Version, BookFingerprint char(64), Path, At. - [RT#7] A row is written only after a successful export that passed the gate, stamped with the book fingerprint at that moment.
3. Components (in Prose.Core, executed in the Hub)
3.1 BookFingerprint. Move LogicSweepService.ComputeBookFingerprintAsync (Audit/LogicSweepService.cs:384, currently internal) to a public static BookFingerprint.ComputeAsync. The logic sweep calls the shared copy.
3.2 ReadGateService (Services/ReadGateService.cs, exists). Changes:
- [RT#3] EntityChanged derives a beat's mentions from
BeatMarkup.ExtractEntityGuids(beat.Text)at computation time, instead of fromBeatEntityMentions. That removes the race with the background tag write (NodeWorkbenchService.cs:453/641). - [RT#4]
NodeWorkbenchService.ExportAudiobookAsync(≈:2995) getsEnsureReadAsyncas its first statement, and ReadGateTests gets a TestCase for it. - The note kinds gain
heard.
3.3 EntityVerificationService (F1: after the read, written only at the end, never resets reads):
BeginAsync(entity, book)returns the canonical record, every mention beat's text (tag-derived, in reading order) and a nonce. The nonce is an HMAC over (entityId, bookId, ModifiedAt, mentionsFingerprint, expiry = now + 2h).CommitAsync(nonce)is refused if ModifiedAt or the mention beats changed since Begin. Otherwise it upserts the row. It never writes the entity, so it doesn't reset any read.
F1 for a unit: every entity tagged in the unit has a verification for this book with RecordModifiedAt = the current ModifiedAt.
3.4 CharacterFieldWriter and cost-visible writes.
SetFieldsAsync(id, fields). Keys are the snake_case namesget_characterreturns. A whitelist covers every writable field: identity, description, psychology, speech_patterns, behavioral, relationships, timeline, cyberware_inventory, neural_abilities, knowledge, conditions, stats, belongings, archetypes, operating_territory, physical_description, genetic_ancestry, ancestry_detail, narration_voice, daily_life, story_hooks, tags, aliases (replace), and the image prompts.- Writing: a value replaces the field,
nullclears it, and an absent key is untouched. Lists are JSON arrays, so commas never split them. - Save path: it saves through
CharacterRepository.Save(Data/Repositories.cs:173-238), thenRefreshReadModelAsync, and returns the fresh record. locationis refused loudly, because it lives inEntityStateEventsuntil §11.create_characterreturns an error, instead ofok:true, when it's givenlocation.- [RT#1] Cost-visible: every entity write (
set_character_fields,create_*update) first computes N, the beats that are currently read and would become unread. If N > 0 andconfirmUnreadisn't true, it refuses with "this edit un-reads N beats (positions …)". That's deterministic friction, and it is logged.
3.5 Save no-op fix [RT#1]. CharacterRepository.Save (Repositories.cs:217) and EfRepository.Save (Data/EfRepository.cs:241) compare the canonical projection before and after. If nothing changed, they skip the bridge wipe and reinsert, and leave ModifiedAt alone. Tests prove that a no-op save leaves ModifiedAt untouched and a real change bumps it.
3.6 RulingService.
RecordAsyncvalidates the regex.ListAsync,SupersedeAsync.FindViolationsAsync(book)runs the activelawpatterns over tag-stripped prose, and over the tagged entities' canonical records.
3.7 MetricsReport [RT#10]. This is book-level, not a per-chapter station.
- It counts each active
metricPattern over the whole book and compares against MaxPer1kWords × bookWords / 1000. - It gates F7 only when metric rulings exist, and those must have Source =
author. - The author sees the targets below as part of this plan's approval:
| Metric | Pattern | Max per 1k words | Book cap on 194k words | V72 count |
|---|---|---|---|---|
| em_dash | — |
7.7 | ≈1,500 | 2,479 |
| not_fragment | (?<=^|[.!?]["”’]?\s)Not [^.!?\n]{1,40}\. |
0.62 | ≈120 | 213 |
| the_way | \bthe way (a|an|you|someone|people|men|a man)\b |
0.46 | ≈90 | 151 |
| whole_of | (that )?was the whole of( it)? |
0.016 | ≈3 | — |
3.8 CaptureScanner (F4) [RT#9, RT#3]. It has two conditions:
(a) Unknown names. A candidate is a run of 1–4 capitalized words (
\p{Lu}[\p{Ll}'’-]+, which may join on of/and/the) that:- occurs at least twice in the book;
- appears mid-sentence at least once;
- is not at the start of a dialogue line;
- isn't all capitals and isn't non-Latin script.
It's resolved by matching, case-insensitive with the possessive stripped, against the book-tagged entities plus universe entities with Status =
canon(not the 12,432-row universe, which includes junk aliases), theincidentalrulings, and a code-level English stop set.(b) Untagged known names. Any occurrence of a name or alias of an entity tagged elsewhere in this book, without a tag in this beat. That's what makes the gate's entity-change coverage complete. It's fixed with
add_entity_tagsor a re-save.
Calibration: the first BCODA run is evidence. If (a) yields more than 150 distinct candidates, the English stop set is widened in code (tested). If F4 isn't green by h18, it ships as NOT COMPUTED (shown in the matrix, never faked).
3.9 ContextBundleService [RT#11]. BuildAsync(book, unit, priorUnits = 1 | all, budgetChars = 400_000) writes one derived file, %LOCALAPPDATA%\Prose\factory\context\<slug>.context.md. It's overwritten every call, sits outside the repo, and is never edited. It contains:
- the active
lawrulings; - the universe's World and Craft canon sections (document types confirmed via
list_canon_document_typesin I6); - the canonical records of the unit's tagged entities and places;
- prior prose, tag-stripped: the previous unit by default, or all preceding units within budget, dropping the oldest first, never reset at a chapter boundary;
- the unit itself: its text, or its planned beats.
It returns a manifest: entity ids and ModifiedAt, the prior range, characters included versus total, what was truncated, and a bundle hash.
- Healing BCODA uses one continuous in-order read, so the read is the context and the default of 1 is enough.
- New units use
all.
3.10 ExportRecorder. Runs after a successful export in DocxExportService.ExportNodeAsync, ManuscriptExportService.Export{Pdf,Epub,AudioTxt}Async and ExportAudiobookAsync. It inserts an Exports row with the current BookFingerprint.
3.11 WorkOrderService. AddAsync refuses an engine order that doesn't descend from an approved root. CloseAsync validates the checks (§5.2), writes the evidence, and closes a parent automatically when all its children are closed. AbandonAsync(reason). FactoryRepoPath = D:\Projects\MindAttic\Prose.
3.12 FactorySessionService. StartAsync records the start action and returns the last summary and the other active sessions (no end, and a ledger call in the last 30 minutes). EndAsync validates decision references.
3.13 FactoryService. StatusAsync(book) is the matrix: stations, blocking orders, a metrics summary and unused tools. NextAsync(book?), JournalAsync(filter), UsageCheckAsync().
The journal [RT#12] is *_History rows for Beats, BeatNodes, Entities and Characters in the time window, joined by time to the ledger rows and to the receipts, notes, verifications, exports and orders written in that window.
The usage check reads tools marked [FactoryTool(Since = "yyyy-MM-dd")]: any with 0 non-test ledger calls 7 days after Since gets an automatic (idempotent) "use or delete" engine order.
4. The line: stations in execution order, with predicates and invalidation [RT#1 reorder]
| # | Scope | Passes when (computed) | Cleared by |
|---|---|---|---|
| F2 Planned | unit | at least 1 beat, and each has text, or both a Title and a Description | insert_beat(title, description) |
| F3 Written | unit | every beat has non-whitespace text and a LastWriteReason. Under the plan-first gate: every prose beat also has a Description whose DescriptionState is current (its plan, confirmed against its prose as it stands) |
update_beat_text / splice_beats, then update_beat_metadata(description) |
| F4 Captured | unit | CaptureScanner (a) and (b) both = 0 | create_* / add_entity_tags / record_ruling(incidental) |
| F5 Read | unit | ReadGateService shows every beat read as it stands |
read_beats(markRead, readBy) |
| F6 Clean | unit | 0 open defect notes and 0 law-pattern hits in the unit's prose |
splice_beats → re-read → resolve_read_note |
| F1 Verified | unit | every tagged entity is verified at its current ModifiedAt | verify_entity_begin → verify_entity_commit |
| F7 Pressed | book | all units F2–F6 and F1; MetricsReport passes (when author metrics exist); docx, epub and pdf Exports rows at the current BookFingerprint. Under the plan-first gate: no plant/payoff the book holds (registered on it, or on a series above it with an end in it) has an unbound end — checked first, before the units are through |
--export-node; link_plant_beat / link_payoff_beat |
| (A) Audio | book | an mp3 Exports row at the current fingerprint |
export_audiobook (only if I0 passes) |
| (H) Heard | unit | informational, never gating and not part of GO [RT#8] | the author's notes, relayed by Claude and marked as a trust point |
factory_next: the first open blocking order; otherwise units in reading order through F2 → F3 → F4 → F5 → F6 → F1, taking the first station that is actionable. A station whose prerequisite is red is skipped, not reported. Then F7, then A.
Protocol rule [RT#1]: during a read pass, entity corrections are filed as defect notes, not applied. They're applied as one batch after the pass (the cost-visible write shows N), the affected beats are re-read (the gate lists them), and F1 is verified last.
What undoes what (all computed):
| Event | Stations that go red |
|---|---|
| beat edited | F5 at that beat and its seams; F6 re-evaluated; F7 and A (the fingerprint changes) |
| beat inserted, deleted or moved | seams F5, F2/F3 for the new beat, F7, A |
| entity record really changed | F1 for the units that tag it; F5 for the beats that tag it; F7, A. A no-op save changes nothing (§3.5) |
| a law ruling is recorded or superseded | F6 wherever it matches; F7 |
| a new capitalized name, or an untagged known name | F4 for that unit |
5. Interfaces
5.1 Tools
Every tool has an MCP name and a CLI twin, and every call goes to the ledger with Actor stamped. The CLI twins work right after a Hub deploy, without an MCP restart.
| MCP tool | CLI twin | Purpose |
|---|---|---|
factory_status(book) |
--factory status --node X |
the matrix |
factory_next(book?) |
--factory next [--node X] |
the one next action, with its exact calls |
factory_context(book, unit, priorUnits?, budgetChars?) |
--factory context … |
the bundle file plus manifest |
factory_journal(since, until?, book?) |
--factory journal … |
history, ledger and factory rows in the window |
record_ruling, list_rulings, supersede_ruling |
--ruling add / list / supersede |
law |
verify_entity_begin, verify_entity_commit |
--verify-entity begin / commit |
F1 |
set_character_fields(id, fieldsJson, confirmUnread?) |
--set-character-fields --id X --file f.json [--confirm-unread] |
the world write path |
work_order_add / list / close / abandon |
--order … |
orders |
session_start, session_end |
used by hooks and /quicksave |
sessions |
existing, unchanged: read_beats(markRead), read_status, add/list/resolve_read_note, splice_beats, insert_beat, update_beat_text, create_*, get_*, add_entity_tags, set_canon_section, export_node, export_audiobook |
existing twins |
5.2 Work-order checks (validated Hub-side)
commit:git merge-base --is-ancestor <hash> HEAD, and the changed files match PathsJson.tests {names[], trx}: the TRX shows every named test Passed, with StartTime later than the commit time.ledger {handler, method?, minCalls}: non-test calls since the order opened, from CommandLedgerEntries.factory {book, station, scope}: the station passes.deploy:/api/healthbuild differs from the one recorded when the order opened.author: an author-attributed item exists. This is a trust point (§8), used only for taste and for approving roots.
5.3 Hub HTTP (local only)
GET /api/factory/next?format=block|lineGET /api/factory/orders?status=open&kind=enginePOST /api/factory/session/startandPOST /api/factory/session/endGET /api/factory/status?node=
6. Session lifecycle and hooks [RT#13, RT#14, RT#17]
| Hook | Script | Behaviour | Failure mode |
|---|---|---|---|
| SessionStart | .prose/hooks/factory-start.ps1 |
Reads stdin session_id. POSTs session/start, GETs next?format=block. Prints the FACTORY NEXT ACTION (with id), LAST SESSION, OTHER ACTIVE SESSIONS, and the laws plus forbidden list |
Hub unreachable: "FACTORY UNREACHABLE — run.ps1 -Apps Hub -Start Hub; do no story work from memory". Exit 0. 10s timeout |
| UserPromptSubmit | .prose/hooks/factory-reminder.ps1 |
One line from next?format=line, cached 60s in %TEMP% |
Hub down: "factory unreachable". 5s timeout |
| Stop (installed after I1) | .prose/hooks/factory-guard.ps1 |
(1) git status --porcelain --untracked-files=all: every changed or ?? repo path must match PathsJson of an open engine order that descends from an approved root. (2) A scan of the session transcript's (transcript_path) Bash/PowerShell tool inputs for sqlcmd with UPDATE, INSERT or DELETE. A violation writes stderr and exits 2 |
Hub down: warning, exit 0. Honours stop_hook_active. Blocks once per distinct violation set (hash in %TEMP%), then warns: no loops |
| PostToolUse (Write|Edit) | .prose/hooks/memory-nudge.ps1 |
For a memory-directory path, injects: "Canon belongs in the world: record_ruling / set_character_fields + read-back." | none |
- Removed:
inject-digest.ps1(the stale 37.6KB digest) andquickload-on-do.ps1. - Kept:
start-prose-hub.ps1andbuild-prose-mcp.ps1. - Redefined:
/quicksavecallssession_end. "do" //quickloadis the start summary./queueiswork_order_add(kind: author). - Archived:
.prose/quicksave.md*andagent-queue.json*go to.prose/archive/. - MEMORY.md is rewritten as a ≤6KB index: a pointer to the factory, the collaboration feedback, and the gotchas. The remaining index lines go to ARCHIVE_INDEX.md. The 662 files aren't curated; they're simply no longer indexed.
7. Protocols (the heartbeat)
- A. Author ruling intake.
- If it's law, style or a metric:
record_ruling(verbatim, with a pattern where mechanical). - If it's an entity fact: write it to the record (cost-visible), confirm the read-back, then verify begin → commit (after the read, if one is in progress).
- Reply with the ids and the read-back.
- If it's law, style or a metric:
- B. Heal a book (one continuous in-order pass):
factory_next.- For each unit in order:
read_beats(markRead), checking the text against the bundle's records and law. - Prose defects:
add_read_note, thensplice_beats(dry run, then apply), re-read,resolve_read_note. - Entity errors: a
defectnote only (the record stays frozen during the pass). - Capture: fix F4 (tags and entities).
- After the pass: batch the entity corrections (cost-visible), re-read the listed beats, verify every tagged entity (F1).
- Export (F7).
- C. Create a book (plan-first: the plan and the book are one row):
- Canon first: the cast, places, factions and things the book needs exist as entities (protocol A) before a beat names them.
create_serieswhen the book is one of several;record_ruling(kind: gate, text: plan-first)on the series (or the book); series-wide laws on the series node.create_bookand its chapters; every beat of every book of the series asinsert_beat(title, description)(F2). A Description says who is present, what changes, and what it plants or pays off.- Every arc as
register_plant_payoff— on the series node when its ends sit in different books — bound at both ends to planned beats. - Read the plan whole and in order (Law 6), book by book and across the series; fix by editing the beats.
- Per chapter:
factory_context(priorUnits: all), write in-session throughupdate_beat_text, then reconcile each beat withupdate_beat_metadata(description)restating what the prose now does (F3 under the gate). Where the prose diverged from its plan, edit the downstream planned beats its edges point to, in the same sitting. - Capture (F4: new entities through protocol A, or incidental rulings).
- Then protocol B.
- At each chapter's end, write what the writing taught about planning into protocol G.
- D. Engine change:
work_order_addunder an approved root, with paths and checks.- Edit,
dotnet test --logger trx, commit withWO:<id>, deploy if Hub code changed. work_order_close(Hub-validated).- Real use (the
ledgercheck).
- E. Press and listen: F7, then A (if I0 passed). The author listens; their notes are relayed as
heardordefectnotes marked relayed. - F. Session end:
/quicksavecallssession_end, which is refused until every decision references an id. - G. Upgrade a book with no formal structure (bring an existing book into the protocol C format; author order 01a12351). Grown from what writing the first plan-first series teaches; distilled into
/upgrade-bookbefore the upgrade campaign begins.- Measure: the strict-F3 split (missing / unreconciled / stale) and the unbound edges, per unit.
- Per chapter, read whole and in context (Law 6); restate every prose beat's Description against its prose as it stands with
update_beat_metadata. Keep the ones already current. By hand, in session — no loop tool, nowrite_synopsis, no LLM service. - Bind or bring to the author every plant/payoff with an unbound end; register the arcs the read surfaces.
- The prose is not touched. A beat that cannot be described honestly is a Rule #1 defect (
add_read_note, kind defect) for its own pass. - When F3 and F7's edge check pass,
record_ruling(kind: gate, text: plan-first)on the book so it cannot regress.
- Lessons from the plan-first series (appended per chapter):
- Planning the Cyberocracy series (CYB1-3, 431 planned beats, 2026-10-10):
- Grain. About six planned beats per chapter, each one scene segment. A Description names who is present, what changes, and what it PLANTS or PAYS OFF, by chapter. Opening beats carry the POV and what the cold reader must learn there.
- Insertion order.
insert_beatwith an emptyafterBeatIdprepends. Inserting a chapter's beats last-to-first gives the right order with no id bookkeeping, and chapters can be filled in any order. Cost: beat#Nnumbers run in reverse within a chapter. - Canon first, as the plan names it. Naming a minor character in a Description means creating the entity at that moment.
find_entitiescaught a first-name collision (Fenwick) before any prose existed, at the cost of one Description edit. - The plan is cheap to change because it is one row. A late reveal (Ambrose soothed Seven, not a child) needed four upstream Description edits and nothing else. The same change after prose would have needed splices and re-reads.
- Mistakes in order are cheap too. A beat inserted in the wrong position is fixed by
delete_beatplusinsert_beatwithafterBeatId, before it has prose. - Edges last. A cross-book arc can be registered only once both ends exist, so plan every book first, then register the series edges on the series node with both beat ids. Record the key arcs as edges; minor within-chapter echoes can stay in Descriptions.
- One decision per ruling. Only the author's premise, the lifted secrecy, the mecha rule, the ending, and the gate were recorded as rulings. Everything else lives in beats and entity records, so nothing parallels the plan.
- For upgrading old books: an existing book's prose already decides what a beat does, so its Description is a restatement, not a plan. That is the same row and the same F3 check, but its job is to describe what is there, never to plan anew.
- Writing the Cyberocracy series (CYB1-3 written, 431 beats, ~288k words, F2/F3/F4/F6 24/24 each, 2026-10-10; order 01a1258b):
- Order of operations per chapter: write, then capture, then reconcile. Entity tags live inside
Beat.Text, so every pin or retag changes TextHash and makes the Description stale. Reconcile last. A tag fix found later (a wrong-entity surname, a vocabulary false positive) stales every touched beat; re-sending the unchanged Description re-stamps it. - Gate hole: a re-sent Description is indistinguishable from a reconciliation. Re-stamping is honest only for beats reconciled before the tag change. Re-sending a planned Description stamps a never-reconciled beat "current" and passes strict F3. It happened once, to one chapter, and was caught by hand. Strict F3 cannot see it. Re-stamp only by chapter range, never book-wide.
- The plan held at the beat grain. A minority of beats diverged in prose (they were not counted). The divergences were cheap: a timeline order the plan got wrong ("the night before the assault" planned two chapters early), a minor name changed, a scene's witness corrected. In each case the Description was restated on the same row. No downstream edit was needed, because the edges pointed at beats rather than at facts.
- The recurring writer defects are knowable in advance, so check them per chapter: cross-POV knowledge (a POV citing a scene it never saw), backstory contradicting the entity record (read
get_characterbefore a POV chapter, not after), plan language leaking into prose ("the cost of the book", "since Wells", "the series' method"), book titles used as in-world references, the "did not decide to" tic, and page-law words (coin, dock, grey, harbor, HR). - Capture traps. A sentence-start capital ("Since Erie", "Not Seven", "Every Schism", "Then Seven", "The Speaker") reads as an unresolved name, so reword it. A common-word alias (One, Seven) must never be pinned case-insensitively. Pin longest names first, or "DELVE" splits "DELVE-9". New minor names collide with the universe's 13k names (Wren, Marisol and Teodor all existed), so run
find_entitiesbefore naming anyone. Series-scoped incidentals resolve F4, but they do not stop the scanner from re-deriving a tag; for that, reword or use a book-scoped incidental. - What carried the books was the edges, not the Descriptions. The 24 cross-book plant/payoff pairs (the count, the tubing, the log, "nobody asked for it back", "I know the way") were what each later book was written toward. The Descriptions kept each beat honest; the edges kept the series whole.
- For the upgrade campaign: start by reading the book's entity records and its last finished chapter's prose, not its old outline. Restate Descriptions in chapter-sized batches with capture already done. Never run a book-wide re-stamp.
- Order of operations per chapter: write, then capture, then reconcile. Entity tags live inside
- Upgrading the first eight GLMZ books (CRIT, ATTE, MxG, BLST, NxR, TEST, TLC, IxS; about 5,000 beats, 2026-10-10; order 01a1272e):
- The read is the yield. Writing a Description forces every beat to be read on its own and against its neighbours, and that surfaces Rule #1 defects no earlier read caught. Defects filed: CRIT 1, ATTE 1, MxG 1, BLST 2, NxR 2, TEST 5, TLC 3, IxS 23.
- Number and clock contradictions (36 vs 32 minutes, "eight days" two days later, a 48-hour timeout with "eight hours" left).
- Dropped threads (an ambush raised and never resolved; a deck the plan needs and never reaches).
- Location slips (a wall the scene isn't behind; a cluster in two buildings).
- Misattributed callbacks ("Boiler's two words" that were Scout's).
- Offstage scenes treated as already known. The longest, earliest-drafted books carry the most. The defects are filed, not fixed, so the gate can pass while F6 holds the book.
- Grain follows the old book, not the plan. Old books run from about 15 to over 1,100 beats, often a sentence or a line of dialogue each. A one-line beat gets a one-clause Description; a chapter's opening beat gets the label
Opening of Chapter N, Title.Do not pad, and do not merge beats to fit the plan grain (that would touch the prose). - Write a chapter's Descriptions as a list against a dump of that chapter, then check the alignment before sending anything. The recurring error is drift: one beat that holds two paragraphs gets two entries, and every Description after it lands one beat late. It happened about ten times across the eight books.
- A deterministic word-overlap check per chapter (an entry matching the next beat better than its own; list length against beat count) catches it before the push.
- Fix by merging the two entries, then recheck.
- The check is scratch arithmetic, not a judge (Law 7), and it changes nothing in the Hub.
- A NULL TextHash keeps a beat "unreconciled" forever. The fix is to re-save the identical text (
--beat update --file), which restamps the hash (order 01a12612), then send the Description again. - The old plan's edges are unbound and often wrong. Books planned before the gate carry plant/payoff rows with no beat ids, written against the outline: a character's old name ("Casimir" for Ekow), a scene in the wrong chapter, a fact the page changed. The page wins.
- Run
get_plant_payoffsbefore registering anything. - Bind each old row to the beats that actually plant and pay it off (
link_plant_beat/link_payoff_beat). - Correct its text with
update_plant_payoff. - Only then register the arcs the read surfaced. Registering first produced duplicates in IxS, because there is no delete.
- Run
- Verify every beat number you cite inside a note's text against the dump. Two IxS notes cited neighbours of the right beat and had to be resolved and re-filed.
- Throughput.
--beat metaruns at a few seconds per beat and the CLI is one process at a time, so a 1,000-beat book is hours of pushing. Run books serially in one background queue and log each "metadata updated". Gate a book only after its log count equals its beat count andfactory_statusshows strict F3 passing.
- The read is the yield. Writing a Description forces every beat to be read on its own and against its neighbours, and that surfaces Rule #1 defects no earlier read caught. Defects filed: CRIT 1, ATTE 1, MxG 1, BLST 2, NxR 2, TEST 5, TLC 3, IxS 23.
- Rewriting a gated book from the premise up (UNDR, 14 chapters, 84 beats, ~42k words, F2-F6 14/14, 2026-10-11; order 01a12c7a):
- Canon first, then archive, then read the old book whole, then plan. The premise lived in entity records, not rulings: the faction, the place and the hero. So the rewrite began as record edits (each read back), and each edit un-read the beats that tag it, in this book and in others.
- Lay the rewrite over the old chapter nodes. Rename each one, run
--beat clear, and insert the planned beats in order with--after.clearkeeps every plant/payoff row and only nulls its beat anchors, so re-anchor the old pairs withlink_plant_beat/link_payoff_beatand correct their text withupdate_plant_payoff. Register only the arcs that are new. Nothing is duplicated, and F7's edge check stays honest. - Read the unit's laws before writing, not after. The context file's page-laws caught three plan-level conflicts that would have been prose defects: a burner (no devices), a credstick carrying a name (credsticks are anonymous by law), and an outside-the-GLMZ city named on the page.
- Decide every count before the chapter that states it. The old book's ending count was inconsistent. Write the arithmetic down (who is counted, and whether the outsider is) and keep it with the plan. Each count line in the prose then checks against it, not against memory.
- Review the F1 records before the final F5 read, not after. Every record fix un-reads every beat that tags the entity, so fixing records after the read means reading again. Order: write and reconcile all chapters, review and fix every tagged record against the book, read the whole book once, then begin and commit F1. If a record fix lands after the read anyway, and the prose has not changed, confirm with a deterministic diff (tag-stripped Hub text against the files that were read) before re-marking. The diff is arithmetic, not a judge.
- Records written for the old premise lie quietly. F1 found seven records contradicting the new book: two characters still "alive" who die on the page, a rival's arc the rewrite dropped, a place whose "worst air" became good air, an example line the prose changed, and a fare item renamed. The F1 begin packet is where they surface; read every record, not only the protagonist's.
- Vocabulary capture traps. A sentence-start "Salt", "Current" or "The Braves" binds to an unrelated slang vocabulary entry or flags as an unresolved name. Vocabulary records take no aliases, so "Gulch" and "Wash" without their article need pinning every chapter, and a pin also re-saves the beats pinned before it, staling their Descriptions. When the scanner keeps re-deriving a wrong tag across a book (here "the Current" as slang instead of DEEP CURRENT), record a book-scoped incidental ruling first, then retag and pin. Without the ruling, every save puts the wrong tag back.
- A book-wide re-stamp is honest only when every beat was reconciled first. Here all 84 had been reconciled against their prose in the same session before a book-wide retag staled every chapter but one, so re-sending the stored Descriptions (fetched in one
get_bookof the book node, pushed with--beat meta) was a true re-stamp. In any book where that is not certain, the chapter-range rule above stands. - The F5 read still finds the writer's slips. In 42k words written and reconciled the same day: a scene at the wrong cistern, two streets swapped, "the night before" for two nights, a count stated two ways in one beat, a sick child "two levels down" from a Hollow far below, an ungrammatical opening line. Each was fixed at the point of confusion and the beat re-read.
- Planning the Cyberocracy series (CYB1-3, 431 planned beats, 2026-10-10):
8. Enforcement matrix
| Failure mode | Mechanism | Strength |
|---|---|---|
| forgets between sessions | SessionStart injects the live next action and last summary from the database | enforced |
| stale instructions | digest and quickload hooks removed; PROSE_PROTOCOL / commit.md fixed; review-node / completeness / storm removed; MEMORY.md index ≤6KB | enforced |
| builds unused or unplanned code | Stop guard (paths must match an order under an approved root); order close needs real ledger use; usage check opens delete orders | enforced (roots are author-approved) |
| changes unrecorded | story writes only through the Hub (ledger plus temporal history); repo writes need orders and commits; raw sqlcmd writes block the Stop hook; the journal reconstructs everything |
enforced; Writer-UI edits are visible through temporal history |
| claims done | stations computed; order checks validated Hub-side | enforced |
| decisions left in chat | session_end refuses unreferenced decisions; memory nudge |
enforced at the boundary |
| ruling drift | no entity facts in Rulings; law patterns at F6; records verified at F1 | enforced |
| writer blind | context bundle, never reset at a chapter boundary (tested) | enforced |
| unwritable or silent entity fields | whitelist coverage (reflection test); location refused loudly |
enforced |
| accidental mass unread | cost-visible writes with confirmUnread |
enforced friction |
| gate bypass | no override; audiobook export gated too | enforced |
| machine prose | book-level metrics gate F7 (author-approved) | enforced |
| LLM judging LLM | none in the factory; forbidden in the start banner; wiring test asserts no LLM dependency in FactoryService |
enforced |
Trust points, stated rather than hidden:
- A read receipt certifies delivery, not understanding.
- A verification certifies that a record was examined against delivered beats, not that it is correct.
- Author-attributed items (root approvals, heard notes) are relayed by Claude.
Mitigations:
- deterministic law, capture and metrics;
- the author's ear;
- the journal, which the author can audit;
- every item carries a session id and is labelled
relayed.
9. Tests (in src/Prose.UnitTests, SQLite fixtures in the style of ReadGateTests)
Existing, kept: ReadGateTests (plus TestCases for the audiobook gate and tag-derived mentions), BeatSpliceTests, WriterFunnelTests.
New:
FactoryStationTests: a pass and a fail fixture for each of F1–F7 and A, plus every invalidation row, plus thefactory_nextskip-when-blocked rule.SaveNoOpTests: a no-op save keeps ModifiedAt; a real change bumps it (characters and EfRepository types).EntityVerificationTests: the happy path; refused when ModifiedAt changed; refused when mentions changed; refused on an expired nonce; commit doesn't touch the entity.CharacterFieldWriterTests: reflection coverage; round-trip; null clears; absent keys untouched; commas kept;locationrefused; cost-visible refusal when N > 0.RulingServiceTests: a bad regex is refused; violations are found in prose and in records; superseded rulings are inert.MetricsReportTests: each seed pattern gives the exact counts on fixtures; the book threshold.CaptureScannerTests: (a) the rules for twice, mid-sentence, dialogue-initial, all-caps, non-Latin, incidental, canon-only matching; (b) an untagged known name is flagged.ContextBundleTests: prior prose across chapter boundaries; budget drops the oldest first; the manifest is complete.WorkOrderServiceTests: commit in a temporary git repo; TRX fixture; ledger counts; factory check; deploy; approved-root enforcement; parent auto-close.FactorySessionTests:session_endgives 422 on an unreferenced decision.ExportRecorderTests: a row is written with the fingerprint; F7 flips when the prose changes.FactoryWiringTests[RT#15, narrowed]: every station is registered and has both a pass and a fail test; thesettings.jsonhooks exist and no digest hook is present;FactoryService's dependency graph contains noILlmService.
Hooks are thin wrappers over tested endpoints. Each is proven by its run output in §10 ([RT#14]: no claude -p grading).
10. Build (incremental, used immediately; hours from approval)
The rule: each increment is one engine order. It isn't done until its checks validate, including real use on BCODA. Never build more than one increment at a time.
| Inc | Hours | Builds | Real use (the proof it's used) | Checks |
|---|---|---|---|---|
| I0 | h0–h2 | nothing. Measure the baseline first (the author: "find the working and state before you start suggesting features") | Safety: a WIP backup of the uncommitted tree. git switch -c backup/wip-2026-09-23, then git add -A && git commit -m "WIP snapshot before factory", then git switch master, then git checkout backup/wip-2026-09-23 -- . [RT#5]. Re-measure every "unknown" and "unverified" baseline row:• B2: dotnet build on each project, recording errors per project• B19: dotnet test --logger trx, recording pass/fail counts• B18: TTS smoke test (Kokoro install, or the ElevenLabs key and credits; one real chapter to MP3) • B5: read_status beat count• B21: open the Writer UI once and run a KdpPublish dry listing, only if I9 or a publish is attempted Update §0.5 with the measured values |
the backup branch exists and the master tree is identical to it; every B-row has a measured value and its command output (backfilled as evidence into the I2 order). If a measurement contradicts this RFC, fix the RFC before building |
| I1 | h1–h6 | finish removal stage 1 (the list is in C:\Users\ryand\.claude\plans\immutable-enchanting-lighthouse-agent-ae76b46847a16e8e8.md). Stage 2 is deferred until after GO [RT#6] |
full build, full tests, commit, deploy | 0 build errors; 0 test failures; git status clean; /api/health build changed |
| I2 | h6–h10 | migration AddNovelFactory; WorkOrderService; FactorySessionService; FactoryService (F2, F3, F5); the §5.3 endpoints; tools and CLI twins; the SessionStart and reminder hooks (the Stop guard after this commit); Actor stamping in Cli/ToolDispatch; retire digest, quickload and queue; fix PROSE_PROTOCOL, commit.md and skills; rewrite the MEMORY.md index; commit this RFC |
seed the build tree (I0–I8 plus leaves) as WorkOrders under the approved root; factory_status BCODA (F5 red for every unit); one real session through start → end |
TRX green; running factory-start.ps1 prints a next-action id and the forbidden list; the guard exits 2 on a temporary FooCli.cs and 0 once it's matched |
| I3 | h10–h13 | RulingService, MetricsReport, F6 |
record every author ruling (enumerated from the memory files, each as a law or an entity fact routed to its record list for I4) and the 4 metric rulings; run F6 and metrics on BCODA | list_rulings count equals the enumerated count; F6 and metrics reports produced |
| I4 | h12–h17 | the save no-op fix; CharacterFieldWriter plus cost-visible writes; EntityVerificationService; F1; the create_character location error; the audiobook gate; tag-derived mentions in the ReadGate |
world pass before any reading [RT#1]: every cast entity (≥3 BCODA tags, 76 entities across 11 types) is corrected against its mention beats. That covers Kyle (mother, hands, piezo, Seo as mentor), Pixel, War Dog, Chen ("Mrs." alias, random ancestry), and age 0 across the cast. No verifying yet | the law scan over records = 0 hits; per-entity read-backs are in the journal |
| I5 | h16–h19 | CaptureScanner, F4 |
the BCODA calibration run; resolve names and tags | the calibration evidence; F4 green, or NOT COMPUTED |
| I6 | h18–h21 | ContextBundleService, ExportRecorder, F7, journal, usage check, BookFingerprint moved |
the bundle for BCODA unit 1; the journal of the I2–I5 sessions | ContextBundleTests; the journal reconstructs I4 |
| I7 | h21–h40 | none (the heartbeat only) | heal BCODA through protocol B: one in-order pass → batch entity fixes → re-read the listed beats → verify all → export V73 | factory_status BCODA: F2–F6 and F1 green for every unit, and F7 green; V73 exists |
| I8 | h30–h42, only if I7 is past unit 30 by h30 | none | a new life: a 3-chapter GLMZ story through protocol C | its factory_status: F1–F7 green; the journal shows every step |
| I9 | h38–h46, only if I0 passed | none | audio: BCODA Ch1–3 and the new story (A) | mp3 Exports rows exist; the files play |
Stop rules:
- A station not green by its increment's end is marked NOT COMPUTED in the matrix. It is never faked, and the work continues by protocol, by hand.
- Metrics not met by h36: the author decides whether to supersede them. Metrics never block the read.
GO at h48: I1–I7 closed with validated checks, and factory_status BCODA green through F7, with V73 pressed through an unoverridden gate. I8 and I9 count if their conditions are met.
NO-GO: the factory's own status and journal are the report. No new systems.
11. After GO (each becomes a branch of engine orders under an author-approved root)
- Removal stage 2: the ledger code and the writer's feeds from it.
- One canonical store per entity: fold
EntityStateEvents(location) andWoundLedgerinto Character; lift the location refusal. - One drop migration for PENDING DROP tables, after a backup and with the author's consent.
- The autonomous writer: rebuilt on
ContextBundleService, or deleted, decided from I8's evidence. - F4 precision tuning if it shipped NOT COMPUTED.
- More books through the same line. The world grows by capture, which is how the factory remembers characters, motivations and events across books.
12. Verification (end to end)
dotnet build: 0 errors.dotnet test: 0 failures across §9. The TRX files are attached to their orders.- The Hub
/api/healthbuild changes after each deploy. factory-start.ps1output shows the Hub's next-action id and the forbidden list. The guard blocks unmatched repo changes and rawsqlcmdwrites.prose --factory status --node BCODAshows every unit green through F7, and the V73 files exist with anExportsrow at the current fingerprint.prose --factory journal --since <I1 start>reconstructs every change: history rows, ledger calls and commits.- Every I1–I7 work order is closed with Hub-validated evidence.
13. As built: where the build differs from the text above
Each note below records a decision made while building an increment, with its reason, so this document stays the design that is actually running.
I4 (the world true)
- The no-op guard covers every relational repository, not just the two §3.5 names. All 29
Saveoverrides inRepositories.csopen withSaveGuard.IsUnchanged: the item is compared with the repository's ownLoadOneprojection, and with theEntitiesrow's name and (where that save syncs it) description. A unit test fails the build if an override loses the guard. The baseEfRepository.Savecompares its JSON blob. set_character_fieldsuses JSON Merge Patch (RFC 7396). Objects merge one level at a time, so changingbehavioral.habitsleavesbehavioral.decision_rulesalone. Lists and plain values replace.nullresets a member to empty and removes a dictionary key. Every write is read back: a field that did not land makes the call fail, never return ok.- F1 waits for the full read. While any beat of the book is unread, F1 reports
waiting, notfail, andfactory_nextgoes on to the reading. This is how [RT#1]'s deadlock is avoided in the computed line itself, not only in the protocol. - F1 includes the mentions fingerprint. A verification is current only while the record's
ModifiedAtand the fingerprint of the beats that tag it (which beats, at which text) both match. Editing a beat therefore voids the verification of every entity that beat tags. - Commit also requires the reading. It is refused while any mention beat is unread. That makes a verification mean "the record was delivered after its beats were read", not only "the record was delivered".
- Records are held to the law.
lawpatterns now bind the canonical records of the entities a book tags, as well as the prose (--ruling violations --records, MCPrecord_law_violations).verify_entity_commitis refused while the record breaks one. - A new ruling kind,
page-law, binds the prose only. It is for facts the world holds but the page never says, such as Seo making Silence, or Mrs. Chen's own child. Its pattern would otherwise flag the very records that are meant to hold the fact. - Sessions resume on compaction. A second SessionStart under the same Claude session id resumes that session's row. Before this, it opened a new row and reported the session to itself as "another session may be working in this tree".
I5 (capture)
- F4 uses the save path's own scan, not a new one. Part (a) is
EntityMentionScanner.FindUnresolvedProperNounsover the tag-stripped text. Part (b) uses the same candidate index, plus the tags a save pins, restricted to entities the book already tags. So F4 can never disagree with what a save would tag. - Part (a) lives within its limits. A name counts only once it is used in at least 2 beats. Words capitalized only by their format (Street, Avenue, North, weekday plurals and similar) are a stop set in code, with tests. No threshold was widened to make the numbers pass: every residue from the BCODA calibration run was resolved by an alias, an
incidentalruling or a named pin. - A name the universe makes ambiguous is resolved from the book. When the book already tags that exact surface as one entity (
BookSays),--pinextends that tag to the untagged uses.--pin-name "<name>" --entity <id>is the named decision that one surface means one entity throughout the book. All of these are tag-only saves through the one door (TagMaintenance). - Memoized. The scan is cached against a fingerprint of its inputs: beat ids and text hashes, the entity table's stamp, and the incidental rulings. That makes it cheap enough to run inside every
factory_status. - Name drift is a question, not an alias. When one person carries two names (Brennan Parr and Brennan Drum, Ezra Fonu and Ezra Vance), capture files a read
questionfor the author. An alias would have hidden the contradiction.
I6 (working memory and the press)
- What the bundle's canon is. glmz holds WorldMaster, WorldBible and UniverseCraft. UniverseCanon is Entos's. The shared CraftGuide, CharacterDoctrine and DelightGuide are read from
Universe.SharedId, because UniverseCraft's sections extend them. EngineGuide and Franchise are left out. The WorldBible still carries about 40KB of the old engine's architecture (§4, §11–14), which a writer doesn't need. Moving it is the author's call, not a section filter the bundle would have to maintain. - The unit's own text is placed first in the budget. The prose before the unit gets what is left, newest first. A unit that fits only partly keeps its tail, marked
…. The manifest states what was dropped. - Presses are recorded for every format: docx, epub, pdf, txt, and mp3 or wav. Station A is book-level: audio of any node of the book at the current fingerprint passes it. Per-node audio coverage would need a
NodeIdonExports(after GO). - The journal reads SQL Server's temporal history for
BeatsandEntities. A record's field-level history stays withEntityHistoryService. On SQLite the journal says it could not look. With a book, a ledger call is attributed to it only when its arguments name the book's slug, code or id. A call that named only a chapter is not attributed, and this limit is stated here rather than guessed around. - The usage check. Each MCP
…Implcarries[FactoryTool(name, since, Cli = …)]. A call through either door counts as use; failed calls and test actors do not. The check runs at every session start. It files "Use or delete:" under the open RFC 0015 root once per tool, identified by title. The order closes only by a ledger check, or is abandoned with the commit that deleted the tool. A wiring test fails the build if any Implon a factory tool class lacks the attribute.
Plan-first (author order 01a12351, engine order 01a12351-4159, 2026-10-09)
- The plan is the beat, not a document. A beat is born planned (Title + Description, no Text), receives its prose in place, and its Description is restated against that prose on the same row (
DescriptionHashstamped,DescriptionStatecurrent). With one row there is nothing to drift but that row's own stale Description, which is computed. - A new ruling kind,
gate, switches a factory gate on for a book, a series above it, or the universe; its text names the gate (plan-first), and it has no pattern. Books without it keep the old F3 and F7, so existing books are not pulled onto the line before their upgrade (protocol G) — the gate is recorded on each as it is finished, then universe-wide. - Rulings are ancestor-scoped.
RulingService.ListAsyncmatches the book and every ancestor, so one ruling on a series node binds each book under it and no other — how the GLMZ secrecy laws are lifted for one series alone. - No dangling check. A deleted beat clears the plant/payoff ends that pointed at it (
NodeWorkbenchServiceclears them; the foreign key is NoAction), so an end is only ever bound or null. F7's unbound-end check covers a planned beat that was deleted or split away.
I7 (BCODA healed, V73 pressed 2026-09-23 14:39Z)
- Protocol B ran as written. One in-order read of all 521 beats with its prose fixes spliced as it went; then one entity batch; then the gate's list re-read (328 beats, then 64 more after the last record fixes); then F1 for all 114 tagged entities; then the press. Every unit is green F2–F6 and F1, F7 passes at the current fingerprint, and no station was overridden.
- An incidental ruling now also suppresses the surface in that book's scan (
EntityMentionScanner, 2c111fb56). A common word that happens to be a vocabulary term or a handle's fragment ("Cut", "Heat", "Rig", "the south side") is fixed with a BCODAincidentalruling and--factory capture --retag-name <surface> --from <id>. The removal reports exit 3 if the scan would put the tag back. A tag on the wrong referent moves with--to <id>. Pins keep a moved tag in place across saves. - A first-name collision is fixed in the prose, not the record. Following the author's precedent (the fleshed-out character keeps the name), BCODA's minor people were renamed, each new name checked unique across the corpus with
--find-entity: Freya→Odile, Marcus Tsui→Horace Tsui, Dex→Jory, Rook→Lintel, Renata→Yaaba Osei-Bonsu, Voss→Dents, Emre→Kojo, Echo→Sonar and Farida Okonkwo→Zuleika Kowalczuk. One-scene names get anincidentalruling; recurring ones get a book-scoped entity (originNodeSlug), which wins the surface in its own book. - A record fix is checked against the whole book, not the beat that raised it. Examining records against their mention packets found facts the book contradicts. Examples: Mrs. Chen's "delivery" covenant and dead son; the Atlas array's 2194 install; Operator Five dead (he walked); "Carrion Enterprises"; Mercer's "sixty-one ghost signatures". The fixes are field writes read back through
set_character_fields/set_entity_fields.locationstill lives inEntityStateEvents, so it is corrected with--fix-location-aspect. - Relationships compare as a set in the read-back (fe73fcc6f). The store loads them sorted by name, so a correct write had reported
NotLanded.--factory capture --retagnow exits on the scan its own repair produced. - A prose line that explains away an apparent error is suspect. "Antiquity & Stationary" was defended on the page as deliberate. The author ruled it a typo: the spelling is now Stationery everywhere, the explanation is gone, and a
lawpattern (01a0ce92) keeps it from coming back. - What F4 still cannot see: a name of three letters or fewer (Cho, Lin, Mei, KT), or one used in a single beat, is never flagged. A surname shared with another book's character (Cho) therefore passes unmarked. Nor can it see a mention tagged to the wrong record: in the BCODA reader's read, a splice re-derived "Stash" onto another book's Dr. Jude "Stash" Adeyemi instead of BCODA's Doc Stash, and F4 passed because the mention was tagged, only to the wrong record. F1 verification caught it (#20351). The author later ruled the two Stashes one person; the limit stands. All three are recorded as limits, not patched with a new instrument (author, 2026-09-23, order 01a0d073).