Files
deepseek-harness/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md
T
Tianyi Cui e6fad266a6 docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.

Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.

Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.

The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00

2.8 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 in deriveMessages (closing the request/adapter aliasing path) plus a dev-mode Object.freeze + invariants plugin. The proposal text is kept for the record.

Make immutability part of the type where mutation is corruption:

  • SessionEvent data becomes DeepReadonly on the way OUT of a session (events, session/event listeners); append() keeps taking plain mutable input. A DeepReadonly<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 the agent/request waterfall (mutation there is sanctioned — the clone makes the boundary explicit and cheap, once per step).
  • PromptAssembly stays mutable through its waterfall (sanctioned) but the registry's internal section list is cloned per assembly (already true).
  • Optionally, dev-mode Object.freeze of 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.