`score_hot` hardcoded `age_hours = 24.0`, so the divisor in
`log10(max(views,1)) / (age_hours + 2)^gravity` was constant across the candidate
set and `Sort::Hot` reduced EXACTLY to `log10(max(views, 1))` -- a view-count
ranking wearing a recency sort's name. Four built-in profiles use it (`hot`,
`for_you`, `following`, `brief`); anyone tuning `gravity` was tuning a no-op.
The in-code comment justified this by saying a per-entity `created_at` lookup
needs an `EntityId -> created_at_ns` reverse map that "is not built". That was
stale, and it was the load-bearing claim: `created_at` has been materialized INTO
item metadata on every write since `Items::metadata_with_created_at`, the executor
has held an `EntityId -> metadata` map since M6p3, and the replication record
carries the materialized map so replicas cannot diverge. No index, storage change,
schema change or migration -- the scorer reads the map it already had, exactly the
way `read_duration` does three lines away.
`Sort::New` used `entity_id as f64`. Wrong twice: it assumed IDs are assigned in
creation order, and it used the ID's MAGNITUDE as the base score, so on a catalog
of N items the sort contributed ~N against a boost sum in single digits. Recency
did not participate in the ranking, it annihilated every boost. Now negated age in
hours -- same ordering, boost-comparable scale.
Three more instances of the same defect class, found by auditing rather than
assuming the report was complete:
1. Both age sorts were missing from `needs_metadata_for_sort`, so a profile with
no session and no diversity never loaded the map the fix depends on.
2. Every metadata sort was DEAD on the SEARCH path. Its metadata pre-load was
gated on `session_context.is_some()` and never consulted `profile.sort`, AND
the `ProfileExecutor` it built never had `with_item_metadata` called at all --
the map it did compute went only to the keyword-hint argument, which the sort
scorers do not read. `shortest`/`longest` scored NEG_INFINITY and the
alphabetical sorts the missing-title sentinel, for every candidate, silently.
3. Under `ReducedCandidates` load the candidate cap kept the highest entity IDs,
correct only while `Sort::New` meant "highest ID". Left alone it would discard
the genuinely newest items BEFORE scoring -- wrong only when degraded, the
hardest case to notice. Now keyed off the `created_at` index via the new
`RangeIndex::top_n_descending`.
The decision "which sorts read item metadata" now lives on `Sort` itself as an
exhaustive match. It was a `matches!` in one executor while a second executor had
its own different copy, which is precisely how a metadata-reading sort came to be
omitted from both.
MEASURED, not inferred:
- Real server, 10 items, equal views, ages 2-20 days: before every score was 0.5
(all-equal set folded to the normalizer's midpoint) and the feed returned
oldest-first forever; after, 1.0 -> 0.0 strictly descending, newest first.
- `new` with zero signals returns the exact REVERSE of candidate-scan order.
- `alphabetical_asc`, `shortest`, `longest` verified end to end with title and
duration order both opposing entity id.
- Metadata point-read cost at 2,000 candidates (the ceiling: `scan_candidates`
caps at `max(limit*10, 200)` and `limit > 500` is rejected): 7.25ms, 3.6us per
candidate. Guarded at 250ms.
THE BUG REPORT'S CENTRAL PROMISE IS FALSE and the changelog says so. §7 claimed
this fix lets a zero-signal corpus rank newest-first so a consumer could delete
its workaround. It arithmetically cannot: the numerator `log10(max(views,1))` is
exactly 0.0 for 0 OR 1 views, so the age divisor has nothing to scale and every
candidate still ties -- confirmed on the live server, all ten scores 0.5.
Age-awareness begins at the second view. Fixing cold-start needs recency to be
ADDITIVE rather than a pure divisor, which reorders every existing Hot consumer,
so it is a separate decision. `sort_hot_zero_view_corpus_still_ties_regardless_of_
age` pins the limit so it cannot be rediscovered by accident.
Three existing tests asserted the old entity-ID behaviour. Inverted to assert real
recency, not loosened -- and each fixture now makes id order and creation order
DISAGREE, because an ordering assertion where the two candidate orderings agree is
satisfied by the defect too. Three of my own new tests were vacuous for exactly
that reason and were caught by mutation-testing; one was also flaky (it passed in
a 12-test run and failed run alone, because retrieval order for exactly-tied
vectors is not deterministic). Every new assertion is mutation-proven against the
implementation it replaces.
Full lib suite 2130 passed. Clippy 66 warnings vs 66 at baseline, zero added.
Reconciles two independently-developed lines from base 006d3d0:
ours — M9/M10 community layers, retroactive purge + re-materialization,
signal revocation, agent capability boundaries, P1 feedback loop,
reason labels, instrumented metrics
theirs — M11/M12 cluster mode (tidal-net gRPC transport, tidal-server
cluster/scatter-gather, tidal-stress), multi-vector preference,
ANN candidate-gen, warm-tier day buckets, keyed signal snapshots
Notable semantic resolutions:
* storage::keys::Tag — both sides allocated 0x0E..0x11 for different
records. Kept theirs' 0x0E..0x1A (shipped on-disk format) and renumbered
ours to 0x1B..0x1E (CommunityMembership/Revocation/PurgeManifest/
CommunityLeave); Tag::ALL grown to 30 so the contiguity drift guard holds.
* ranking executor — took theirs' rewrite (SignalReadPlan pre-pass, keyed
SignalKey snapshots, Result-returning reads, finalize()) and re-applied
ours' M10 read suppression at the chokepoints it introduced:
single_signal_score, score_hot/trending/controversial,
CreatorEngagementRate, and the Stage-4 boost loop.
* signals::warm — theirs' day-bucket/read-time-rotation rewrite, with ours'
subtract_bucket and Clone extended to the new day tier; ours' test split
kept (warm/tests.rs, warm/proptests.rs) carrying theirs' updated bodies.
* db::signals — kept ours' contribution-logging try_cohort_attribution in
signal_dispatch.rs and theirs' event-time try_update_preference_vector;
dropped the superseded duplicates.
* db::mod / from_parts — theirs' constructors, with ours' purge/
re-materialization/revocation/community/skip-counter fields and restart
rebuilds; from_parts kept in its own file per the 600-line guideline.
* schema::validation::builders — ours' module split with theirs' expanded
tests; policy validation runs both sides' checks (read-signal lists +
profile overrides, then the zero-duration limit guard).
* feedback Unhide no longer writes a -1.0 "hide" signal: theirs' engine
rejects negative weights (spec §8). Reverses index state only, matching
every other undo action.
* SessionState::new is now the single construction path (gains
overrides_rejected/default_profile); AuditEntry gains kind on the
deserialize path, inferred from the accepted flag as before.
* Removed tidal/src/replication/tcp_transport.rs and its test: never
declared in replication/mod.rs on either branch, so it had never
compiled and nothing referenced it. Superseded by tidal-net's
GrpcTransport.
Verified: cargo clippy -p tidaldb (lib) clean; --all-targets compiles for
tidaldb/tidal-net/tidal-server/tidal-stress; 2094/2094 lib tests and the
integration suite pass except m8p3_reconcile_production's two CRDT-count
assertions, which fail identically on MERGE_HEAD (pre-existing).
tidalctl cannot build locally: its aws-sdk deps need rustc 1.91.1, local
toolchain is 1.91.0.
Replace ScoredCandidate.signal_snapshot Vec<(String,f64)> with
SmallVec<[(SignalKey,f64); 4]> where SignalKey is Static(&'static str)
| Owned(Arc<str>):
- Compile-time-constant labels (sort bases, relevance, co_engagement,
preference_affinity) -> Static: zero allocation, pointer-copy clone.
- Dynamic {signal}_boost/_penalty/_decay labels built once per query in a
RuleLabels hoist (was format! per-candidate-per-rule), shared by Arc.
Cohort rescore hoisted the same way.
- scored accumulator pre-sized to candidates.len().
- Owned Strings rebuilt only at the two response-assembly sites (<= limit).
Byte-identical output: full 1896-test lib suite green. Measured win
(cargo bench --bench ranking, committed-base vs working-tree):
score_200_hot 23.89->21.04us (-11.9%), score_200_trending
27.66->25.85us (-6.5%), score_200_full_pipeline 27.88->26.97us (-3.3%).
smallvec promoted from the lock to a direct dep (no new dependency
surface). perf-sweep doc updated; T2 (per-term DashMap collapse) next.
Resolves every finding in docs/reviews/M0-M10-code-review-2026-06-08-pass2.md
across the engine, network, server, and CLI crates: session restore,
replication/CRDT, WAL format and recovery, storage indexes, query/ranking
executors, cohort/community governance, and scatter-gather routing.
Adds regression tests:
- review_pass2_creator_search_filter
- review_pass2_d_replication
- review_pass2_query_for_session
- review_pass2_storage_indexes_bitmap_cache
- review_pass2_zone_a_sessions
Verified: cargo clippy -D warnings and full test suite green across all crates.
- Eliminate the tidal/ self-contained doc mirror; docs now have two canonical
homes (root *.md and docs/), with planning/specs/research/reviews moved up
- Remove stale .agents/skills and .ai mirrors; canonicalize skills under .claude/
- Add pre-commit hook + scripts/check-docs.sh doc-guard + scripts/install-hooks.sh
- Implement M0-M10 seven-dimension review findings across engine, net, server,
and tidalctl (durability, replication, query, WAL, storage, CLI hardening)
Resolves the 142 findings from tidal/docs/reviews/CODE_REVIEW_m0-m10.md across
the engine, server, net, and CLI surfaces:
- WAL/session-journal durability, checkpoint format, and crash-recovery hardening
- Replication shipper/receiver, tenant isolation, and migration paths
- Cluster scatter-gather, router, standalone server + health/offload endpoints
- tidalctl refactored into command modules with JSON output and WAL-state tooling
- Cohort, governance, signal-ledger, and vector-registry correctness fixes
- Expanded UAT/integration/durability test coverage across all milestones