Add a second axis to every RFC — its class (feature, bug-fix,
simplification, architecture, process, testing) — encoded in the path
as docs/rfc/{lifecycle}/{class}/file.md. The folder is the label, so
the closed set is enforced by structure rather than a parsed field.
Two new doc-sync gates back it:
- verify-rfc-classification: every RFC sits in a valid class folder and
the README index lists it under the matching lifecycle→class heading.
- verify-doc-refs: every docs/*.md path cited in a packages|examples TS
comment resolves — closes a drift class verify-md-links can't see, and
catches the four comment refs this reorg moved.
The README gains a Classification section explaining the taxonomy and
per-class index sub-sections. A self-referential process RFC records why
the scheme is path-encoded and gated.
2.9 KiB
RFC: Deep-readonly public surfaces
Status: rejected — the pervasive DeepReadonly<T> type flip was rejected in favor of an always-on deriveMessages clone plus dev-mode Object.freeze + invariants. The immutability goal shipped via that alternative; see dev-mode invariants.
Problem
The session log is append-only by contract, but session.events returns readonly SessionEvent[] whose elements are mutable: a plugin can reach in and rewrite history (events[0].data.content.push(...)), silently breaking replay equivalence and the derived-history guarantee. The same applies to derived messages and prompt assemblies passed through waterfalls — mutation is sometimes the intended idiom (waterfall middleware mutates the request) and sometimes corruption (mutating a logged event), and the types don't distinguish.
Proposal
Implemented differently — see the Status line and dev-mode invariants. The
DeepReadonly<T>design below was rejected as written (compile-only, high type-noise, castable). What shipped: an always-on deep clone inderiveMessages(closing the request/adapter aliasing path) plus a dev-modeObject.freeze+ invariants plugin. The proposal text is kept for the record.
Make immutability part of the type where mutation is corruption:
SessionEventdata becomesDeepReadonlyon the way OUT of a session (events,session/eventlisteners);append()keeps taking plain mutable input. ADeepReadonly<T>utility type lands in dsh-llm next to the brand/never helpers.deriveMessages()returns deep-readonly messages; the loop clones before handing a mutable request to theagent/requestwaterfall (mutation there is sanctioned — the clone makes the boundary explicit and cheap, once per step).PromptAssemblystays mutable through its waterfall (sanctioned) but the registry's internal section list is cloned per assembly (already true).- Optionally, dev-mode
Object.freezeof event data behind the dev-mode invariants flag, so sanctioned-mutation violations throw in tests rather than corrupting silently.
Plan
Introduce DeepReadonly, flip the session read paths, fix resulting compile errors in consumers (expected: a handful in tests), add the freeze-in-dev option alongside the dev-mode invariants plugin.
Risks
DeepReadonly types can produce noisy errors at waterfall boundaries where mutation IS the API — keep the mutable/readonly boundary exactly at "logged vs in-flight" and document it in the session README.