fix: comment retention taxonomy — TTL by (post policy x signing class)

Retention was keyed on comment type, so public and post-key-signed comments
carried a randomized 30-365d expiry inside their signed digest. Under the
ruling neither should ever expire; only the private, non-post-key-signed
(open-slot/greeting) channel gets an automatic TTL, since that is the
throwaway-identity retirement mechanism.

New crates/core/src/comment_ttl.rs is the single authority: CommentClass
(Public / PostKeySigned / OpenSlot / Unverifiable) x CommentTtlRule
(Never | Window | UnknownParent), with draw_expiry (writer) and ttl_ok
(holder). expires_at_ms == 0 is the never-expires sentinel, honored by the
sweep, the ingest gate, and the store_comment upsert.

- Post.comment_ttl: Option<CommentTtlPolicy> — a GENERIC per-post policy, so
  an author-set TTL is a future config surface, not a redesign. Registry posts
  declare a flat 30d policy that binds EVERY comment on them (registrations,
  duplicate reports, anything else), replacing the registration-only rule.
- OpenSlotDecl.max_comments: author-declarable cap on private PK-unsigned
  comments, enforced holder-side (clamped to the holder default), replacing
  the hardcoded per-bio greeting cap. Refusal remains "declare no slot".
  Node::set_greetings_max + `greetings-max` CLI command to write it.
- Holder enforcement rejects TTLs contradicting the parent's policy in both
  directions; a comment naming a different post than its envelope is rejected.
- UnknownParent rule: bounded TTLs accepted from unheld parents (self-heal),
  never-expires refused — permanence is not granted on unseen evidence.

Also fixed while here: five Post-reconstructing queries silently dropped
comment_ttl AND the pre-existing fof_gating (shipped in v0.8.0-alpha), so any
gated or policy-carrying post failed BLAKE3 verification on sync/export and
was discarded with no diagnostic. All hydration now goes through one
POST_COLUMNS/post_from_row path; export/import round-trips the policy.

Registry frozen bytes regenerated for the policy field; REGISTRY_POST_ID is
now 10a1be3383efb2977607fe45c4a7b3f1b5e626e81d0ac1af9c0f3d7eb9864d32.
design.html section 21 rewritten to the corrected taxonomy.

250 core tests (was 228); a3 integration 12/12 (new step 6 asserts registry
comments hold exactly 30d while greetings randomize); c_topology 33/33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
This commit is contained in:
Scott Reimers 2026-08-03 15:33:44 -04:00
parent fa02ace4cc
commit 8b042f6598
20 changed files with 1676 additions and 255 deletions

View file

@ -12,7 +12,13 @@
# unsealed text + real sender persona; node2 replies; node3 sees the
# reply. node1 (holder) stores only ciphertext.
# 5. Newest-wins: node2 re-registers; node3 sees exactly one entry.
# 6. (Optional, ITSGOIN_TEST_TTL_SECS) comment expiry sweep.
# 6. Retention taxonomy (per-post policy x signing class): every comment
# on the registry post carries the post's FLAT 30d policy — including
# on holders that never registered — while greetings on a bio's open
# slot carry the randomized 30-365d stranger TTL. Asserted against
# each node's DB, so it also covers the debug ITSGOIN_TEST_TTL_SECS
# hook staying scoped to randomized windows (a flat policy must keep
# its exact 30 days, or the registry auto-renew loop never settles).
#
# Exit code 0 = all checks passed. Logs: /tmp/itsgoin-cli{1,2,3}.log
set -u
@ -137,6 +143,38 @@ check "node3 sees exactly one (newest) entry for node2's persona" \
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 6 | grep -c "rust" | grep -q "^1$" &&
quiet_tail /tmp/itsgoin-cli3.log 6 | grep -q AliceV2'
echo "== step 6: retention taxonomy =="
# (parent post's comment-TTL policy) x (comment's signing class).
DAY_MS=86400000
FLAT30=$((30 * DAY_MS))
YEAR365=$((365 * DAY_MS))
reg_hex=$(grep shipped_constant /tmp/itsgoin-genesis.log | awk '{print $2}' | tr 'a-z' 'A-Z')
bio2_hex=$(echo "$bio2" | tr 'a-z' 'A-Z')
reg_total=0; reg_bad=0
for i in 1 2 3; do
reg_total=$((reg_total + $(sq $i "SELECT count(*) FROM comments WHERE hex(post_id)='$reg_hex'")))
reg_bad=$((reg_bad + $(sq $i "SELECT count(*) FROM comments WHERE hex(post_id)='$reg_hex'
AND (expires_at IS NULL OR expires_at - timestamp_ms != $FLAT30)")))
done
check "every registry-post comment carries the post's flat 30d policy" \
bash -c "[ $reg_total -gt 0 ] && [ $reg_bad -eq 0 ]"
greet_total=$(sq 2 "SELECT count(*) FROM comments WHERE hex(post_id)='$bio2_hex'")
greet_ok=$(sq 2 "SELECT count(*) FROM comments WHERE hex(post_id)='$bio2_hex'
AND expires_at IS NOT NULL
AND expires_at - timestamp_ms BETWEEN $FLAT30 AND $YEAR365")
check "greetings on the bio open slot carry the randomized 30-365d TTL" \
bash -c "[ $greet_total -gt 0 ] && [ $greet_ok -eq $greet_total ]"
over_cap=0
for i in 1 2 3; do
over_cap=$((over_cap + $(sq $i "SELECT count(*) FROM comments
WHERE expires_at IS NOT NULL AND expires_at - timestamp_ms > $((366 * DAY_MS))")))
done
check "no stored comment claims a TTL past the 366d holder ceiling" \
bash -c "[ $over_cap -eq 0 ]"
echo
echo "== results: $PASS passed, $FAIL failed =="
[ "$FAIL" -eq 0 ]