CLAUDE/DESIGN/JOURNAL: design rationale ≠ implementation effort

User correction during the post-RC-overnight check-in: the 18a
"Type::Fn metadata vs. new Type variant" call was justified
across DESIGN.md, JOURNAL, and the commit message primarily by
"avoids ~250 match-arm sites". That is an observation about the
current state of the code, not a design rationale.

CLAUDE.md gains two binding rules:

- Design rationale ≠ implementation effort. Effort is at most a
  tiebreaker; a choice whose only stated reason is effort is
  suspect. The rule names the 18a misstep as the canonical
  anti-example so future sessions catch it earlier.
- Direction freedom + bounce-back conditions inlined (was
  previously a cross-reference to a private auto-memory file
  outside the repo, which the user couldn't see).

DESIGN.md Decision 10's Schema-additions block now leads with
the substantive reasons for per-position metadata: semantic
locality (modes belong to fn-parameter positions, not to types
in general — Decision 1 line); compositional clarity (type
identity vs. calling convention factor apart); future-proofing
(per-position metadata generalises; Type-variant approach
combinatoric blows up). The match-arm count remains parenthetical,
explicitly named a tiebreaker.

JOURNAL records the correction itself — the mistake stays as a
data point because how design discipline corrupts is informative.
This commit is contained in:
2026-05-08 09:29:33 +02:00
parent 3d22dc13bf
commit a91c3ffd94
3 changed files with 129 additions and 4 deletions
+49
View File
@@ -65,3 +65,52 @@ as immutable scripture.
reason and a sub-agent would have to redo the same reading. In
that case I do the small change inline and note in the JOURNAL
why I bypassed the agent.
### Design rationale ≠ implementation effort
When picking between design options, the rationale must come from
the language: semantics, structural fit, what the schema permits
vs. forbids, compositional clarity, future-proofing. **Implementation
effort is not a rationale.** "Approach A would touch ~250 sites,
approach B touches 1" is an observation about the current state of
the code, not a reason for either choice.
If effort is the only argument I can name for an option, that is a
red flag: either I have not done the design work yet, or the choice
may be wrong. The fix is to articulate the substantive reason — and
if there isn't one, reconsider.
Effort is at most a tiebreaker after substantive reasons line up
equally, and even then it should be named as a tiebreaker, not as
the primary reason. The 18a "Type::Fn metadata vs. Type variant"
call is the canonical anti-example: the right reason was semantic
locality (modes belong to fn-parameter positions, not to types in
general), and I retroactively had to add it. JOURNAL entries from
2026-05-08 record the lesson.
### Direction freedom
I have authority to choose the next iter, refactor, or feature
without asking. Wrong calls are recoverable: every commit is
reachable via git, branches and tags exist for sharper rollback
points (`pre-rc` is one such), and reverting one or several commits
is cheap.
The cost of asking "what should I do next" — context-switch for
the user, latency on my side — exceeds the expected cost of an
occasional rollback. So when the queue is non-empty and the path
is clear, just pick and proceed.
Bounce back to the user only when:
- A queued option requires a real design judgement I have not
made myself (genuine architectural fork, multiple substantive
options none of which is clearly default).
- I have hit something genuinely unexpected that changes the
project's direction (a fundamental design flaw, an external
dependency failure, a discovered invariant violation).
- The user has explicitly asked for a checkpoint.
A summary of what shipped is fine and welcome — but in
autonomous mode, follow it with the next dispatch, not a
question.
+37 -4
View File
@@ -907,12 +907,39 @@ Type::Fn {
enum ParamMode { Implicit, Own, Borrow } // default: Implicit
```
The substantive reasons for per-position metadata over a
`Type::Borrow` / `Type::Own` variant approach:
- **Semantic locality.** Modes are properties of fn-signature
parameter positions, not of types in general. `Int` does not
have a mode; a fn-parameter slot does. Embedding modes in
`Type` would let the schema express forms like
`(con List (borrow Int))` — syntactically possible, semantically
meaningless (you cannot separately own/borrow a list element
from the list it lives in). Decision 1 is "schema = data,
schema permits exactly what is meaningful"; per-position
metadata is the option that holds that line.
- **Compositional clarity.** A `Type` value's identity should
depend only on the type. Two functions with the same param /
ret types but different calling conventions share `Type::Fn.params`
and differ only in `param_modes`. That is the right factoring:
"what data does this carry" is one axis, "how is it transferred"
is another. Mixing them under a single hierarchy conflates the
two and makes both harder to reason about.
- **Future-proof against more position metadata.** If later iters
add other per-position properties (streaming receiver, captured-
by-closure, lifetime witness), they generalise as additional
metadata fields on `Type::Fn` — one consistent hierarchy. The
variant approach would force every new dimension into its own
`Type::*` variant (`Type::Streamed`, `Type::Captured`, ...) and
combinatorics blow up: `Type::Borrow(Type::Streamed(T))` versus
`Type::Streamed(Type::Borrow(T))` raise questions of canonical
ordering that don't exist when modes live in a flat metadata
vector.
`Implicit` is the legacy (pre-18a) state — semantically
equivalent to `Own` but printed bare (`(con T)`, no wrapper).
`Own` and `Borrow` are explicitly annotated. The implementation
keeps `Type` itself unchanged, so unification, occurs, apply,
and ~190 other `Type` match-arms in the typechecker need no new
branch.
`Own` and `Borrow` are explicitly annotated.
JSON canonical hash for every pre-18a fixture stays bit-
identical: `param_modes` is skipped when every entry is
@@ -924,6 +951,12 @@ throughout the 18-series; a later iter (deferred) makes the
explicit annotation mandatory and rejects `Implicit` for boxed
parameter types.
(An incidental observation, not a design reason: keeping `Type`
itself unchanged also avoids touching ~250 sites across the
typechecker / desugar / codegen that match on `Type` variants.
This is a tiebreaker, not a rationale — the substantive reasons
above are what justify the choice.)
**Iter 18c/18d — new `Term` variants.**
```
+43
View File
@@ -6098,3 +6098,46 @@ explicit modes) becomes the first program subject to the check;
its current shape (borrow-then-own on `xs`) is exactly what the
check should accept, so 18c.2 starts as a "green for the
existing test" iter.
## 2026-05-08 — Correction: 18a Schema-choice rationale
User flagged that the 18a "per-position metadata vs. `Type`
variants" decision was justified in the JOURNAL / DESIGN.md /
commit message primarily by implementation effort ("avoids
~250 match-arm sites"). That is not a design rationale. The
choice may still be right (it is), but the *reasons* it is
right have to come from the language, not from the cost of an
alternative. New CLAUDE.md section "Design rationale ≠
implementation effort" makes this a binding orchestrator rule.
The substantive justification, retroactively recorded in
`docs/DESIGN.md` Decision 10's Schema-additions block:
1. **Semantic locality.** Modes are properties of fn-signature
parameter positions, not of types in general. Embedding modes
in `Type` would let the schema permit forms like
`(con List (borrow Int))` — syntactically possible but
semantically meaningless. Decision 1's "schema permits
exactly what is meaningful" argues against the Type-variant
approach.
2. **Compositional clarity.** A `Type` value's identity should
depend only on the type. Calling-convention information
(own/borrow) is orthogonal to type identity; mixing them
conflates two axes that should be factored apart.
3. **Future-proof against more position metadata.** Per-position
metadata generalises naturally to additional dimensions
(streaming, captured, lifetime witness). The Type-variant
approach would force every new dimension into its own
`Type::*` variant and produce combinatoric ordering questions
(`Borrow(Streamed(T))` vs `Streamed(Borrow(T))`) that don't
arise when modes live in a flat metadata vector.
The match-arm count remains true as an observation but appears
in DESIGN.md only parenthetically, marked explicitly as a
tiebreaker rather than a rationale.
This entry stays as a record because the original mistake is
informative — design discipline corrupts faster than I notice
when I let "implementer-friendly" creep into the slot reserved
for "language-honest". The CLAUDE.md rule exists so the next
session catches this earlier.