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:
@@ -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
@@ -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.**
|
||||
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user