Make an aggregate's own events a first-class mode

An aggregate with no default_sink already left events created in it alone, but
treated doing so as a failure: it reported a skip per event per cycle saying no
sink was configured, as though something had gone wrong. Nothing had. An
aggregate is also a calendar, and holding events of its own is a legitimate way
to use one.

Skipped::NoSink is replaced by a kept_local count, reported plainly. `@local`
joins the routing markers, so the mode also works per-event where a
default_sink is configured — which was not previously expressible. Unlike every
other marker it is deliberately not stripped: the others have done their job
once the event reaches its source, whereas this one never leaves, so it has to
stay legible for the next cycle to reach the same decision. `local` is
therefore a reserved endpoint id, and configuring one is refused.

This also fixes a real defect. `retarget` rebuilds the new target from the
recorded links, which cover derived events only, so an event belonging to the
aggregate itself did not follow the move — it stayed on the calendar being left
behind while everything around it moved on, quietly. It is now carried across,
since there is nothing to re-derive it from, and the new target's scheduling
rule is applied on the way: this is a write to an aggregate like any other, and
a guest list carried live onto a server that schedules would mail everyone on
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-09-10 14:30:25 +03:00
parent 7607078394
commit 47ad8b4c47
9 changed files with 298 additions and 26 deletions

View file

@ -144,6 +144,9 @@ reported in one pass:
- `target` must not also be one of its own `sources`.
- `sources` must be non-empty, and each must exist and be named once.
- `default_sink`, if given, must be one of the `sources` and must be writable.
Leaving it out is a deliberate mode, not an omission — see
[Events of the aggregate's own](#events-of-the-aggregates-own).
- No endpoint may be called `local`, which is reserved for the routing marker.
### A complete example
@ -256,6 +259,7 @@ aggregate `unified`
new events go to `gcal` unless told otherwise
to choose, put one of these on a line of its own in the event's notes:
@gcal
@local (keep the event here, in this calendar only)
```
Read-only feeds are left out, since they cannot take an event. A marker naming
@ -263,6 +267,31 @@ anything else is refused and reported — the event stays where it is rather tha
being quietly filed under the default, which would put it somewhere you did not
ask for.
### Events of the aggregate's own
An aggregate is also a calendar, and it can hold events that belong to nothing
else. There are two ways to get one:
- **`@local`** on an event, exactly like any other marker, keeps it where it was
written even though a `default_sink` would otherwise have taken it. Unlike
every other marker it is *not* removed from the event: the others have done
their job once the event reaches its source, whereas this one has to stay
legible so every later cycle reaches the same decision.
- **Leave `default_sink` out of the aggregate entirely.** Then nothing is
configured to file a new event under, so every untagged event stays put and
only `@`-tagged ones are sent anywhere. Routing becomes opt-in rather than
opt-out.
`local` is a reserved endpoint id for this reason, and configuring an endpoint
with that name is refused.
One thing to be aware of: every other event in an aggregate is derived from a
source, so the aggregate is disposable — lose the calendar and it rebuilds
itself on the next sync. **An event of the aggregate's own exists in exactly one
place.** Nothing else holds a copy, so it is only as safe as that calendar is.
`aggregate retarget` carries these events to the new target along with
everything else, since there is nothing to re-derive them from.
A **category** matching an endpoint id works too, that being the field
iCalendar intends for this. Be aware that any category on a newly created event
is read as a routing instruction, so an event carrying an unrelated category