Skip to content

Derived ranking keys, local: related / count / sum and the type chain (nxf 6j6v.x0wf) - #48

Closed
cabcookie wants to merge 3 commits into
mainfrom
feat/x0wf-derived-keys
Closed

cabcookie wants to merge 3 commits into
mainfrom
feat/x0wf-derived-keys

Conversation

@cabcookie

Copy link
Copy Markdown
Member

Stacked on #47 (which stacks on #46). Merge those first; until then this diff also shows their commits.

What

The local cut of docs/specs/derived-ranking-keys.md (spec e354, §11 cut 1). A per-type order ([ranking.next.<type>], [ranking.list.<type>], and the [ranking.list] default order) can now hold derived keys alongside field keys:

{ related = "parent", type = "project", of = "rank", nulls = "last" }
{ related = "contributes_to", type = "goal", of = { field = "order" }, pick = "min", nulls = "last" }
{ count = "children", type = "action", status = ["open", "in_progress"], dir = "desc" }
{ sum = [ { count = "mentioned_by", type = "project" },
          { count = "mentioned_by", type = "action", label = "waiting", status = ["open", "in_progress"] } ], dir = "desc" }
  • Load-time validation (spec §7): these are refused, each error naming its key. A key that mixes kinds or names none. An unknown edge kind or direction. A part the kind does not take (pick/of on a count, nulls on a count/sum, status/label on related, derived parts on a field key, precedence on a derived key, dir/nulls inside of). An undeclared type, status or of field (custom fields are allowed and checked against on). of = "rank" on a type with no declared order. A cycle in the type chain, across both sections, with the cycle named. Unknown parts such as weight fail at parse.
  • Bottom-up, once per query (spec §4/§5): read::derived::compute makes one edge pass (Store::present_edges_of_kinds), plus the reverse mentions through q29p's read::mentioners_of (spec §11: one implementation), plus one bulk read of the neighbours (and their labels/custom fields only when a key needs them). It then walks the chain leaves first, with one sort and a dense rank per type, and stores the values the comparator reads. Cost O(E + N log N). A plugin with no derived keys pays nothing.
  • TypeRank carries the values, so next (inside the tiers) and list --sort rank share one comparator.

Readings of the spec, recorded in its new §12 (each is the owner's to confirm)

  1. of = "rank" reads the asking section's per-type order, else the other section's. §8 needs this: persons ranked by list read the rank of projects that only next ranks. Such a list query takes next's candidates at a reference time. That is ListQuery::now / read::list_at, and the CLI and MCP fill it from the clock only for such a plugin. Without one, the query answers validation.
  2. A list rank's scope is the live, unarchived items of the type, whatever the query filters by. next's scope is its candidates before filters.
  3. "Dense rank": items equal on every key share a rank. The id orders the sort, never the rank.
  4. Only live neighbours count: deleted ones drop, closed and archived ones count. This is mentioners_of's rule.
  5. Derived keys are refused in the [ranking.next] default order, which also ranks blocked and the lanes' --sort rank.
  6. An untyped related … of = "rank" in a per-type order is a self-cycle, and the error asks for the neighbour's type.
  7. read::next_active (the store-free path) refuses a plugin whose [ranking.next] has derived keys, until 1c5s.
  8. §8's sum example is not valid TOML 1.0: an inline table cannot break a line outside its arrays.

Facade

facade: breaking. RankKey gains the derived parts, becomes #[non_exhaustive], and refuses unknown parts. Build one in code with RankKey::field_key. Additive: CountPart, OfField, Pick, RankOf, RankSpec::has_derived, ListQuery::now, read::list_at, and in core Store::present_edges_of_kinds / EdgeReadStats (plus Hash on EdgeKind). Fold revisions and the fold-ddb index are untouched.

Tests

  • plugin::tests: each refusal, both cycle shapes, the spec's §8 orders, and the cross-section rank source.
  • crates/facade/tests/derived_ranking.rs: the §8 chain at the Engine seam (next_query, list_query, filters, paging). Then each kind over hand-built boards, determinism under shuffled edge order, the store-free refusal, and a query-shape ratio guard in the xn8s style: edge and item reads are identical at 12 and 60 projects, with one pass per query.
  • Core: present_edges_of_kinds and its counter.
  • Run locally, all green: the facade lib (142), next_rank_per_type, next_query, list_query, mentioned_by, read, derived_ranking, core store::tests, cli list, cargo xtask docs check, cargo xtask changelog check origin/main, cargo fmt --check.
  • No CLI black-box test covers derived keys: the nxf binary links only the bundled plugins, and they declare none. The spec's differential test against graph::Board belongs to 1c5s.

Docs: the plugin guide (EN/DE) has a new "Declaring a ranking" section, commands.md (EN/DE) points to it, and the spec gains §12 plus a note in the tiers spec.

🤖 Generated with Claude Code

https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME

cabcookie and others added 3 commits October 11, 2026 01:47
…v.q29p)

- Engine::mentions(id): the outgoing targets, the existing read::mentions put on the seam.
- Engine::mentioned_by(id, &MentionedByQuery) / mentioned_by_value: the live items that mention
  a target, in id order, filtered by NextFilter (type, container, label) and cut/paged exactly
  like next_query.
- read::mentioners_of(store, targets): THE bulk reverse lookup (over the new
  Store::mentioned_by_bulk) that the derived ranking key count = "mentioned_by" (x0wf) is to
  share, per docs/specs/derived-ranking-keys.md section 11.
- The paging half of next_page is factored into page_through/Paging/Watermark, so next and
  mentioned_by share one snapshot table, expiry, caps and invalidation; the snapshot key names
  the read, so tokens do not cross between them. next's keys are unchanged.
- CLI: nxf mention by <id> [--type]... [--in] [--label] [--limit [--paginate [--token]]], with
  list records and next's --json envelope; goldens under both plugins, guide EN/DE, generated
  docs, verb-seam alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
…e (nxf 6j6v.n698)

- Plugin: [ranking.list] takes the same keys as [ranking.next]: per-type tables
  [ranking.list.<type>], the cross-type order types = [...], an optional default order.
  Parsed and validated by the same code (errors name their section). Ranking::list is
  always resolved at load: absent, or without its own order, it inherits the
  [ranking.next] default order, so list --sort rank is unchanged for every plugin that
  declares nothing (both bundled plugins).
- Ranking: NextRank becomes TypeRank over a &RankSpec; next ranks with
  TypeRank::next, list --sort rank with TypeRank::list - one comparator for both, the
  place derived keys (x0wf) plug in once.
- Paging: read::ListQuery + read::list_page reuse NextFilter (type alternatives,
  container = belongs_to OR contributes_to, label) and q29p's Paging / Watermark /
  page_through; the snapshot key names the read ("list"), status, sort, filter and a
  fingerprint of [ranking.list], so tokens never cross reads or plugin orders.
- Engine::list_query(&ListQuery) -> NextPage and Engine::list_query_value (the
  {"items","total"[,"next_token","restarted"]} envelope of list_value records).
- CLI: nxf list --type (repeatable) --in <id> --limit --paginate --token; plain array
  without --limit, next's envelope with it; human view discloses the cut and prints
  the token. Default order stays id.
- Tests at the seam (facade/tests/list_query.rs, plugin unit tests), CLI black-box
  (cli/tests/list.rs), goldens under both plugins, guide EN/DE, generated docs.

Stacked on #46 (q29p).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
…idation and the type chain (nxf 6j6v.x0wf)

Builds docs/specs/derived-ranking-keys.md on the local SQL path. A per-type order
([ranking.next.<type>], [ranking.list.<type>], and the [ranking.list] default order)
takes three derived keys besides field keys: `related` (a value on the neighbours over
an outbound edge: `of = "rank"` or `of = { field }`, custom fields included, picked by
min/max), `count` (neighbours over an edge kind and direction, filtered by type/status/
label) and `sum` (counts with equal weight).

- Load-time rules (spec §7): kind exclusivity, edge kinds, parts per kind, declared
  types/statuses/fields, `of = "rank"` needs a declared order, and the acyclic type chain
  across both sections, the cycle named. Derived keys are refused in the [ranking.next]
  default order (it also ranks blocked and the lanes).
- Bottom-up once per query (spec §4/§5): one edge pass (Store::present_edges_of_kinds),
  the reverse mentions through q29p's read::mentioners_of (spec §11), one bulk read of
  the neighbours, one sort and a dense rank per type in the chain, then the comparator
  reads precomputed values. A plugin without derived keys pays nothing.
- `of = "rank"` reads the asking section's order, else the other's (spec §8: persons by
  the projects next ranks); a list query then needs `now` (ListQuery::now, read::list_at;
  the CLI and MCP pass the clock only for such a plugin).
- The store-free read::next_active refuses a plugin with derived next keys until 1c5s.
- Tests: plugin load validation (each refusal, cycles), the spec's orders at the Engine
  seam (next_query, list_query), each kind, determinism, and a query-shape ratio guard
  (EdgeReadStats) that the edge pass runs once per query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
cabcookie added a commit that referenced this pull request Oct 11, 2026
…paging, local derived ranking keys (nxf 6j6v.q29p, n698, x0wf) (#49)

Folds #46 (q29p), #47 (n698) and #48 (x0wf) plus the mitigation of their combined independent review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016q5uMUkZJQW4fckJoA3FME
@cabcookie

Copy link
Copy Markdown
Member Author

Contained in #49, which folds #46, #47 and #48 plus the review mitigation and was merged in one squash.

@cabcookie cabcookie closed this Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant