Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions changes/n698-list-ranking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
type: added
facade: changed
---
[en]
`list` can now be ranked per type, narrowed like `next`, and paged. A plugin may declare
`[ranking.list.<type>]` tables (and `types = [...]` for the order of the types, plus an optional
default `order`) under `[ranking.list]` — the same keys, the same load-time checks and the same
comparator as `[ranking.next]`. `nxf list --sort rank` ranks by them; a type without a table, and
every plugin without `[ranking.list]`, ranks as before by the `next` default order. The default
`list` order stays id. `nxf list` also takes `--type` more than once (the types are alternatives),
`--in <id>` (the container's children and contributors), `--limit` (with a `showing <n> of <total>`
notice, and the `{"items", "total"}` envelope under `--json`) and `--paginate` / `--token`, which
page through the same machine-local cache `next` uses. Without `--limit`, `list --json` is the same
plain array as before. An embedding app gets the same read from the engine:
`Engine::list_query(&ListQuery)` and `Engine::list_query_value`; `Ranking::list` carries the
resolved list policy. Everything is additive; no existing signature changes.
[de]
`list` lässt sich jetzt pro Typ ranken, wie `next` eingrenzen und blättern. Ein Plugin kann unter
`[ranking.list]` Tabellen `[ranking.list.<typ>]` deklarieren (dazu `types = [...]` für die
Reihenfolge der Typen und optional eine eigene Standard-`order`) — dieselben Schlüssel, dieselben
Prüfungen beim Laden und derselbe Vergleich wie bei `[ranking.next]`. `nxf list --sort rank` rankt
danach; ein Typ ohne Tabelle und jedes Plugin ohne `[ranking.list]` rankt wie bisher nach der
Standardreihenfolge von `next`. Die Standardreihenfolge von `list` bleibt die Id. `nxf list` nimmt
außerdem `--type` mehrfach (die Typen sind Alternativen), `--in <id>` (die Kinder und Beitragenden
des Containers), `--limit` (mit dem Hinweis `showing <n> of <total>` und unter `--json` der Hülle
`{"items", "total"}`) und `--paginate` / `--token`, die durch denselben maschinenlokalen Cache
blättern wie `next`. Ohne `--limit` ist `list --json` dasselbe schlichte Array wie bisher. Eine
einbettende App bekommt denselben Lesezugriff von der Engine: `Engine::list_query(&ListQuery)` und
`Engine::list_query_value`; `Ranking::list` trägt die aufgelöste Listen-Policy. Alles ist additiv;
keine bestehende Signatur ändert sich.
25 changes: 25 additions & 0 deletions changes/q29p-mention-reads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
type: added
facade: changed
---
[en]
Mentions can now be read in both directions. `nxf mention by <id>` lists the items that mention an
item, in id order, as `list` records (closed and archived mentioners included). `--type`, `--in` and
`--label` narrow the list as they narrow `next`. `--limit` cuts it and says so, and `--paginate`
pages through it with a token, exactly as `next` does. Under `--json` you get a plain array, or
with `--limit` the `{"items", "total"}` envelope, plus `next_token` and `restarted` with
`--paginate`. An embedding app gets the same reads from the engine: `Engine::mentions(id)` returns
an item's outgoing targets, and `Engine::mentioned_by(id, &MentionedByQuery)` /
`Engine::mentioned_by_value` return the items that mention it. `read::mentioners_of` is the bulk
reverse lookup behind them. Everything is additive; no existing signature changes.
[de]
Erwähnungen lassen sich jetzt in beide Richtungen lesen. `nxf mention by <id>` listet die Items, die
ein Item erwähnen, in Id-Reihenfolge und als `list`-Datensätze (geschlossene und archivierte
eingeschlossen). `--type`, `--in` und `--label` grenzen die Liste ein wie bei `next`. `--limit`
kürzt sie und sagt das an, und `--paginate` blättert mit einem Token durch sie, genau wie bei
`next`. Mit `--json` kommt ein schlichtes Array, mit `--limit` die Hülle `{"items", "total"}`, und
mit `--paginate` zusätzlich `next_token` und `restarted`. Eine einbettende App bekommt dieselben
Lesezugriffe von der Engine: `Engine::mentions(id)` liefert die ausgehenden Ziele eines Items,
`Engine::mentioned_by(id, &MentionedByQuery)` und `Engine::mentioned_by_value` die Items, die es
erwähnen. `read::mentioners_of` ist die Massen-Rückwärtssuche dahinter. Alles ist additiv; keine
bestehende Signatur ändert sich.
66 changes: 66 additions & 0 deletions changes/x0wf-derived-ranking-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
type: added
facade: breaking
---
[en]
A plugin can now rank items by their neighbours in the graph, not only by their own fields.
Besides field keys, a per-type order (`[ranking.next.<type>]`, `[ranking.list.<type>]`, and the
`[ranking.list]` default order) takes three derived keys:

- `related` reads a value on the neighbours over one outbound edge: their rank in their own type's
order (`of = "rank"`) or a field, a custom field included (`of = { field = "..." }`), picking the
smallest or largest.
- `count` counts the neighbours over one edge kind and direction (`children`, `contributors`,
`dependents`, `mentioned_by`, or an outbound kind) that pass its `type`, `status` and `label`
filters.
- `sum` adds several counts with equal weight.

So actions can rank by their project's rank, projects by their goals' declared order and their
open children, and persons by how often projects and open waiting actions mention them. Ranks are
computed bottom-up through the chain of types, once per query, with one pass over the edges.
`count = "mentioned_by"` reads the same reverse mention lookup as `nxf mention by`. A plugin is
refused at load when a key mixes kinds, names an unknown edge, type, status or field, carries a
part its kind does not take, reads the rank of a type without an order, or when the types rank by
each other in a cycle. An untyped `related ... of = "rank"` key is refused in the `[ranking.list]`
default order as well, and a table under `[ranking]` other than `next` and `list` is refused rather
than ignored. A `parent` neighbour is the item's one current parent, the one `show` and `--in`
read. Derived keys are not allowed in the `[ranking.next]` default order, which
also ranks `blocked` and the lanes. A plugin without derived keys ranks exactly as before. For an
embedding app: `RankKey` gains the derived parts and is now `#[non_exhaustive]` and refuses
unknown parts (build one with `RankKey::field_key`); `ListQuery::now` and `read::list_at` take
the reference time a `list` order needs when it reads the rank of a type `next` ranks; and the
store-free `read::next_active` refuses a plugin whose `[ranking.next]` has derived keys until the
server path computes them.
[de]
Ein Plugin kann Items jetzt nach ihren Nachbarn im Graphen ranken, nicht nur nach ihren eigenen
Feldern. Neben Feldschlüsseln nimmt eine Reihenfolge pro Typ (`[ranking.next.<typ>]`,
`[ranking.list.<typ>]` und die Standardreihenfolge von `[ranking.list]`) drei abgeleitete
Schlüssel:

- `related` liest einen Wert an den Nachbarn über eine ausgehende Kante: ihren Rang in der
Reihenfolge ihres eigenen Typs (`of = "rank"`) oder ein Feld, auch ein eigenes Feld
(`of = { field = "..." }`), und nimmt den kleinsten oder größten.
- `count` zählt die Nachbarn über eine Kantenart und Richtung (`children`, `contributors`,
`dependents`, `mentioned_by` oder eine ausgehende Art), die seine Filter `type`, `status` und
`label` bestehen.
- `sum` addiert mehrere Zählungen mit gleichem Gewicht.

So können Aktionen nach dem Rang ihres Projekts ranken, Projekte nach der deklarierten
Reihenfolge ihrer Ziele und ihren offenen Kindern, und Personen danach, wie oft Projekte und offene
wartende Aktionen sie erwähnen. Ränge werden von unten durch die Kette der Typen berechnet, einmal
pro Abfrage, mit einem Durchgang über die Kanten. `count = "mentioned_by"` liest dieselbe
Rückwärtssuche der Erwähnungen wie `nxf mention by`. Ein Plugin wird beim Laden abgelehnt, wenn
ein Schlüssel Arten mischt, eine unbekannte Kante, einen unbekannten Typ, Status oder ein
unbekanntes Feld nennt, einen Teil trägt, den seine Art nicht nimmt, den Rang eines Typs ohne
Reihenfolge liest, oder wenn die Typen sich im Kreis gegenseitig ranken. Ein Schlüssel
`related ... of = "rank"` ohne `type` wird auch in der Standardreihenfolge von `[ranking.list]`
abgelehnt, und eine Tabelle unter `[ranking]` außer `next` und `list` wird abgelehnt statt
ignoriert. Ein `parent`-Nachbar ist der eine aktuelle Parent des Items, den `show` und `--in`
lesen. Abgeleitete Schlüssel sind
in der Standardreihenfolge von `[ranking.next]` nicht erlaubt, die auch `blocked` und die Lanes
rankt. Ein Plugin ohne abgeleitete Schlüssel rankt genau wie bisher. Für eine einbettende App:
`RankKey` bekommt die abgeleiteten Teile, ist jetzt `#[non_exhaustive]` und lehnt unbekannte Teile
ab (bauen mit `RankKey::field_key`); `ListQuery::now` und `read::list_at` nehmen die Bezugszeit,
die eine `list`-Reihenfolge braucht, wenn sie den Rang eines Typs liest, den `next` rankt; und das
store-freie `read::next_active` lehnt ein Plugin ab, dessen `[ranking.next]` abgeleitete Schlüssel
hat, bis der Server-Pfad sie berechnet.
36 changes: 35 additions & 1 deletion crates/cli/docs/guide/de/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ $ nxf list --json
[{"archived":null,"assignee":null,"belongs_to":null,"closed_at":null,"closing_comment":null,"completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Cut the first release","design":null,"due":null,"id":"ab12.0001","priority":"1","priority_label":"P1","status":"in_progress","title":"Ship v1","type":"epic","type_label":"epic","updated_at":"2026-06-23T00:00:00Z"},{"archived":null,"assignee":"dev","belongs_to":"ab12.0001","closed_at":null,"closing_comment":null,"completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Build the command-line tool","design":"Thin CLI over the core.","due":"2026-12-31","id":"ab12.0002","priority":"1","priority_label":"P1","status":"in_progress","title":"Write the CLI","type":"feature","type_label":"feature","updated_at":"2026-06-23T00:00:00Z"},{"archived":null,"assignee":null,"belongs_to":null,"closed_at":"2026-06-23T00:00:00Z","closing_comment":"approved","completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Approve the final spec","design":null,"due":null,"id":"ab12.0003","priority":"2","priority_label":"P2","status":"closed","title":"Spec sign-off","type":"feature","type_label":"feature","updated_at":"2026-06-23T00:00:00Z"}]
```

`list --sort rank` rankt stattdessen: nach der `list`-Reihenfolge des Plugins — einer Reihenfolge
pro Typ, wo das Plugin eine deklariert (`[ranking.list.<typ>]`, mit `types = [...]` dafür, wie die
Typen zueinander stehen), sonst nach seiner `next`-Reihenfolge. Eine Reihenfolge kann auch nach den
Nachbarn eines Items ranken — dem Rang seines Elternteils, der Zahl seiner offenen Kinder, wie oft
es erwähnt wird — mit den abgeleiteten Schlüsseln, die unter [Plugins](nxf-plugins) beschrieben
sind. `--type` (mehrfach), `--in <id>`
(die Kinder und Beitragenden des Containers), `--status` und `--label` grenzen ein. `--limit` und
`--paginate` kürzen und blättern genau wie bei `next`: mit `--limit` ist die Antwort `{"items":
[...], "total": <n>}`, und `--paginate` ergänzt `next_token` (mit `--token` zurückgeben; null auf
der letzten Seite) und `restarted`:

```console
$ nxf list --sort rank --limit 2 --paginate --json
{"items":[{"archived":null,"assignee":null,"belongs_to":null,"closed_at":null,"closing_comment":null,"completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Cut the first release","design":null,"due":null,"id":"ab12.0001","priority":"1","priority_label":"P1","status":"in_progress","title":"Ship v1","type":"epic","type_label":"epic","updated_at":"2026-06-23T00:00:00Z"},{"archived":null,"assignee":"dev","belongs_to":"ab12.0001","closed_at":null,"closing_comment":null,"completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Build the command-line tool","design":"Thin CLI over the core.","due":"2026-12-31","id":"ab12.0002","priority":"1","priority_label":"P1","status":"in_progress","title":"Write the CLI","type":"feature","type_label":"feature","updated_at":"2026-06-23T00:00:00Z"}],"next_token":"[..].2","restarted":false,"total":3}
```

`search` matcht über Titel, Beschreibung und Notizen:

```console
Expand Down Expand Up @@ -118,7 +134,7 @@ $ nxf dep remove ab12.0002 ab12.0003 --json
```

`mention add` / `mention list` / `mention remove` halten Freitext-Referenzen per Short-id fest —
ein Verweis, der nie blockiert:
ein Verweis, der nie blockiert. `mention by` liest sie in der Gegenrichtung:

```console
$ nxf mention add ab12.0002 ab12.0001 --json
Expand All @@ -132,6 +148,24 @@ $ nxf mention list ab12.0002 --json

```

`mention by <id>` listet die Items, die `<id>` erwähnen, in Id-Reihenfolge als `list --json`-Datensätze
(geschlossene und archivierte eingeschlossen). `--type`, `--in` und `--label` grenzen ein wie bei
`next`. Ohne `--limit` ist die Antwort ein schlichtes Array; mit `--limit` ist sie `{"items": [...],
"total": <n>}`, und `--paginate` ergänzt `next_token` (null auf der letzten Seite) und `restarted`
(true, wenn ein veraltetes Token wieder die erste Seite lieferte) — genau wie `next --limit --json`:

```console
$ nxf mention by ab12.0001 --json
[{"archived":null,"assignee":"dev","belongs_to":"ab12.0001","closed_at":null,"closing_comment":null,"completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Build the command-line tool","design":"Thin CLI over the core.","due":"2026-12-31","id":"ab12.0002","priority":"1","priority_label":"P1","status":"in_progress","title":"Write the CLI","type":"feature","type_label":"feature","updated_at":"2026-06-23T00:00:00Z"}]

```

```console
$ nxf mention by ab12.0001 --type feature --limit 1 --paginate --json
{"items":[{"archived":null,"assignee":"dev","belongs_to":"ab12.0001","closed_at":null,"closing_comment":null,"completion_criterion":null,"created_at":"2026-06-23T00:00:00Z","defer_until":null,"deleted":null,"description":"Build the command-line tool","design":"Thin CLI over the core.","due":"2026-12-31","id":"ab12.0002","priority":"1","priority_label":"P1","status":"in_progress","title":"Write the CLI","type":"feature","type_label":"feature","updated_at":"2026-06-23T00:00:00Z"}],"next_token":null,"restarted":false,"total":1}

```

```console
$ nxf mention remove ab12.0002 ab12.0001 --json
{"msg":"removed mention ab12.0002 -> ab12.0001","ok":true}
Expand Down
75 changes: 75 additions & 0 deletions crates/cli/docs/guide/de/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,78 @@ Diese Verdopplung ist keine Redundanz — sie *ist* die Erklärung. Das `--json`
das Plugin ist, wie ein bestimmtes Publikum sie liest und rankt. Ein Plugin bei `init` zu wählen
ist daher eine echte Entscheidung; führen Sie `nxf init --help` aus, um die Optionen zu sehen,
bevor Sie sich festlegen.

## Ein Ranking deklarieren

Ein Plugin deklariert unter `[ranking.next]`, wie `next` rankt, und unter `[ranking.list]`, wie
`list --sort rank` rankt. Beide nehmen dieselben Schlüssel und dieselben Regeln:

- `order = [...]` ist die Standardreihenfolge: eine Liste von Schlüsseln, der Reihe nach
verglichen, mit der Id als letztem Entscheider.
- `[ranking.next.<typ>]` (oder `[ranking.list.<typ>]`) gibt einem Typ eine eigene `order`.
- `types = [...]` sagt, wie die Typen zueinander stehen, sobald einige eine eigene Reihenfolge
haben.

Ein Schlüssel ist ein **Feldschlüssel**, etwa `{ field = "priority", dir = "asc" }`. Mit einer
`precedence`-Liste rankt ein Feldschlüssel stattdessen nach der Position des Werts in dieser
Liste. In einer Reihenfolge pro Typ, oder in der Standard-`order` von `[ranking.list]`, kann ein
Schlüssel auch ein **abgeleiteter Schlüssel** sein, der ein Item nach seinen Nachbarn im Graphen
rankt:

```toml
[ranking.next]
types = ["project", "action"]
order = [ { field = "priority" } ]

# related: ein Wert, gelesen an den Nachbarn über eine ausgehende Kante (parent, contributes_to,
# dep, mentions). `of = "rank"` liest die Position des Nachbarn in der Reihenfolge seines eigenen
# Typs; `of = { field = "..." }` liest ein Feld, auch ein eigenes Feld. `pick` ist min (Standard)
# oder max.
[ranking.next.action]
order = [
{ related = "parent", type = "project", of = "rank", nulls = "last" },
{ field = "priority" },
]

# count: wie viele Nachbarn passen. children, contributors, dependents und mentioned_by zählen die
# eingehenden Kanten parent, contributes_to, dep und mentions; eine ausgehende Art zählt in die
# andere Richtung. `type`, `status` (eine Liste) und `label` filtern die gezählten Nachbarn.
[ranking.next.project]
order = [
{ related = "contributes_to", type = "goal", of = { field = "order" }, nulls = "last" },
{ count = "children", type = "action", status = ["open", "in_progress"], dir = "desc" },
]

# sum: mehrere Zählungen, mit gleichem Gewicht addiert. `dir` gehört zur Summe.
[ranking.list]
types = ["person"]
[ranking.list.person]
order = [
{ sum = [ { count = "mentioned_by", type = "project" },
{ count = "mentioned_by", type = "action", label = "waiting" } ], dir = "desc" },
]
```

Abgeleitete Schlüssel zählen nur lebende Nachbarn: ein gelöschter fällt weg, ein geschlossener
oder archivierter zählt. Ein Rang wird über die Kandidaten des Typs unter dem Verb berechnet, das
ihn rankt, und Items, die in jedem Schlüssel gleich sind, teilen sich einen Rang. Für `next` sind
diese Kandidaten seine umsetzbaren Items, für `list` die lebenden, nicht archivierten Items. Ein
Nachbar außerhalb dieser Menge, etwa ein geschlossenes Projekt unter `next`, hat keinen Rang und
liest sich als null. Ein `list`-Schlüssel darf den Rang eines Typs lesen, den nur `next` rankt;
die Kandidaten von `next` werden dann zur aktuellen Zeit genommen (`NXF_NOW` legt sie fest). Nur
`list --sort rank` liest diese Zeit.

Das Plugin wird beim Laden abgelehnt, mit dem Schlüssel benannt, wenn:

- ein Schlüssel Arten mischt oder keine nennt;
- ein Schlüssel eine unbekannte Kantenart, einen unbekannten Typ, Status oder ein unbekanntes Feld
nennt;
- ein Schlüssel einen Teil trägt, den seine Art nicht nimmt, etwa `pick` an einer Zählung oder
`nulls` an einer Zählung oder Summe (eine Zählung ist nie null);
- `of = "rank"` einen Typ ohne eigene Reihenfolge nennt;
- die Typen sich im Kreis gegenseitig ranken.

Ein Schlüssel `related ... of = "rank"` ohne `type` liest den Rang jedes Typs mit einer
Reihenfolge, auch des eigenen. Das ist ein Kreis, also nennen Sie den `type` des Nachbarn; die
Standard-`order` von `[ranking.list]` lehnt einen solchen Schlüssel ebenfalls ab. Abgeleitete Schlüssel sind in der Standard-`order` von `[ranking.next]` nicht erlaubt, weil diese
Reihenfolge auch `blocked` und `--sort rank` der Lanes rankt.
Loading
Loading