agents: name the comment convention, fix two stale pointers
Cite the plan, don't argue it twice — that's the rule, now filed.
It ships beside two pointers of its own that had gone wild:
one factory the constructor ate, one method never there.

Assisted-by: Claude Opus 5 via Claude Code
change zlrwutrqwptmzxspsqzysruqwvlsvoty
commit 1570eac33c5695259f24351be9b74f66b676782c
author Alpha Chen <alpha@kejadlen.dev>
date
parent tmqmxvlq
diff --git a/AGENTS.md b/AGENTS.md
index e4eac56..d2dc76f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -162,10 +162,10 @@ not a fixture; edit a `.vcf` to change what the replay serves.
   what `Store#contact` does for the 404 path. The store loads the
   extension on every open.
 - An etag is derived from the card, never stored beside it: `Contact`
-  hashes what it serves, and `Contact.for` is the only way to make one.
-  The etag in `changes` is the exception and is not the same fact — it
-  is what the card hashed to at that write, which nothing can recompute
-  once the card moves on.
+  hashes what it serves, and its constructor is the only way to make
+  one — no second path takes an etag on trust. The etag in `changes` is
+  the exception and is not the same fact — it is what the card hashed
+  to at that write, which nothing can recompute once the card moves on.
 - Nothing above `vcard/parser.rb` handles a parse error, and nothing
   should start. `Parser.lines` is the only read: a line that will not
   read comes back as a Line with no property. The card is served from
@@ -189,6 +189,14 @@ not a fixture; edit a `.vcf` to change what the replay serves.
   editor, and tested directly rather than through anything that serves.
 - `docs/plans/` entries are dated records of what was decided then. Write
   a new one rather than editing an old one to match current behavior.
+- Comments carry the reasoning; the code carries the rest. One that
+  restates the line below it has nothing to say, and neither does a
+  test comment that restates its test name — the names in `test/` are
+  sentences already. Where the reasoning is already written down, cite
+  it rather than re-derive it: a `docs/plans/` entry and the section
+  within it, an RFC section, or the method that owns the rule. Two
+  copies of an argument drift apart, and the plan holds the whole of
+  it.
 - Types live in the code, as RBS comments: `#:` above a method for its
   type, `# @rbs` for instance variables and skips, `#:` at the end of a
   line for a constant or an assertion. `rake steep` checks them, and
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index a9fad3d..6459622 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -178,7 +178,7 @@ class StoreTest < Minitest::Test
   # sqlite3 gem encodes every bound value to UTF-8, so a binary-flagged
   # string with a byte above 7 bits raises at the bind — loudly, at the
   # contract violation, rather than being quietly relabelled here. The
-  # route is where Rack's binary becomes text (Web#utf8).
+  # route is where Rack's binary becomes text (Web#write_card).
   def test_binary_bytes_above_ascii_raise_at_the_bind
     accented = AIDEN.sub("Aiden", "Aiden Åberg")