@mulmoclaude/core 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/error-recovery.md +85 -0
- package/assets/helps/google-calendar-collection.md +41 -18
- package/dist/collection/core/schemaZ.d.ts +10 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/index.cjs +2 -2
- package/dist/collection/server/index.js +2 -2
- package/dist/collection-watchers/index.cjs +2 -2
- package/dist/collection-watchers/index.js +2 -2
- package/dist/{discovery-_fjsluLx.js → discovery-B4CZvrXR.js} +11 -3
- package/dist/{discovery-_fjsluLx.js.map → discovery-B4CZvrXR.js.map} +1 -1
- package/dist/{discovery-DYcR83qx.cjs → discovery-NRy3tyUA.cjs} +11 -3
- package/dist/{discovery-DYcR83qx.cjs.map → discovery-NRy3tyUA.cjs.map} +1 -1
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/calendar.d.ts +9 -0
- package/dist/google/calendarLock.d.ts +16 -0
- package/dist/google/calendarPushState.d.ts +1 -1
- package/dist/google/collectionProjection.d.ts +18 -0
- package/dist/google/collectionPush.d.ts +62 -0
- package/dist/google/collectionSync.d.ts +51 -23
- package/dist/google/index.cjs +833 -567
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +4 -2
- package/dist/google/index.js +825 -568
- package/dist/google/index.js.map +1 -1
- package/dist/google/pushPlan.d.ts +1 -1
- package/dist/{server-l4TZ5Lbf.cjs → server-7U-3DE2e.cjs} +2 -2
- package/dist/{server-l4TZ5Lbf.cjs.map → server-7U-3DE2e.cjs.map} +1 -1
- package/dist/{server-BlIrtGKl.js → server-DdyIvFhz.js} +2 -2
- package/dist/{server-BlIrtGKl.js.map → server-DdyIvFhz.js.map} +1 -1
- package/package.json +4 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../../rolldown-runtime-D6vf50IK.cjs");
|
|
3
3
|
const require_calendarGrid = require("../../calendarGrid-CaS9er8i.cjs");
|
|
4
|
-
const require_discovery = require("../../discovery-
|
|
5
|
-
const require_server = require("../../server-
|
|
4
|
+
const require_discovery = require("../../discovery-NRy3tyUA.cjs");
|
|
5
|
+
const require_server = require("../../server-7U-3DE2e.cjs");
|
|
6
6
|
const require_feeds_paths = require("../paths.cjs");
|
|
7
7
|
const require_ingestTypes = require("../../ingestTypes-D1GQdG8e.cjs");
|
|
8
8
|
const require_notifier = require("../../notifier-tMsAXyXp.cjs");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../../calendarGrid-CrGxeCOG.js";
|
|
2
|
-
import { N as listItems, V as resolveDataDir, W as safeSlugName, d as storeFor, et as createForwardingLogger, n as discoverCollections, tt as createHostSlot } from "../../discovery-
|
|
3
|
-
import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-
|
|
2
|
+
import { N as listItems, V as resolveDataDir, W as safeSlugName, d as storeFor, et as createForwardingLogger, n as discoverCollections, tt as createHostSlot } from "../../discovery-B4CZvrXR.js";
|
|
3
|
+
import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-DdyIvFhz.js";
|
|
4
4
|
import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
|
|
5
5
|
import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-XReG-li7.js";
|
|
6
6
|
import { d as publish, n as clear } from "../../notifier-BdA5qzhe.js";
|
|
@@ -40,6 +40,7 @@ export type CalendarEventSpan = {
|
|
|
40
40
|
export type CalendarEventInput = {
|
|
41
41
|
summary: string;
|
|
42
42
|
description?: string;
|
|
43
|
+
location?: string;
|
|
43
44
|
/** Calendar to create the event on; defaults to the user's primary. */
|
|
44
45
|
calendarId?: string;
|
|
45
46
|
/** Event colour (Google event palette id "1".."11"); omit to inherit the calendar's colour. */
|
|
@@ -61,6 +62,8 @@ export interface UpdateCalendarEventInput {
|
|
|
61
62
|
end?: CalendarEventTime;
|
|
62
63
|
/** `""` clears the description; omit to leave it untouched. */
|
|
63
64
|
description?: string;
|
|
65
|
+
/** `""` clears the location; omit to leave it untouched. */
|
|
66
|
+
location?: string;
|
|
64
67
|
calendarId?: string;
|
|
65
68
|
colorId?: string;
|
|
66
69
|
/** Etag of the version this edit was computed against. Sent as `If-Match`, so
|
|
@@ -87,6 +90,12 @@ export interface CalendarEventSummary {
|
|
|
87
90
|
status: string;
|
|
88
91
|
/** Google event palette id ("1".."11"), or "" when the event inherits the calendar's colour. */
|
|
89
92
|
colorId: string;
|
|
93
|
+
/** The event body, RAW. Google stores limited HTML here, and normalising it on
|
|
94
|
+
* the way in would silently strip the user's formatting on the way back out —
|
|
95
|
+
* a mirrored calendar has to survive the round trip, so any prettifying
|
|
96
|
+
* belongs to whatever displays it. */
|
|
97
|
+
description: string;
|
|
98
|
+
location: string;
|
|
90
99
|
}
|
|
91
100
|
export interface CalendarSummary {
|
|
92
101
|
id: string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Serialise `run` against whatever is already running for `key`.
|
|
2
|
+
*
|
|
3
|
+
* `locks` is passed in so the queuing rule is testable without module state;
|
|
4
|
+
* the key is dropped once nothing is queued behind it, so the map cannot grow
|
|
5
|
+
* an entry per calendar forever. A failed predecessor still releases the
|
|
6
|
+
* queue — `then(run, run)`. */
|
|
7
|
+
export declare function withKeyedLock<T>(locks: Map<string, Promise<unknown>>, key: string, run: () => Promise<T>): Promise<T>;
|
|
8
|
+
/** Serialise anything that touches ONE calendar's sync state.
|
|
9
|
+
*
|
|
10
|
+
* Shared with the push path (#2598), not only the pull doors: a push that
|
|
11
|
+
* overtakes an in-flight pull would record a baseline for events the pull is
|
|
12
|
+
* still writing, so the next push would diff against a future it never saw.
|
|
13
|
+
*
|
|
14
|
+
* NOT reentrant — a holder must call the lock-free form of whatever it drives,
|
|
15
|
+
* or it waits on itself forever. */
|
|
16
|
+
export declare function withCalendarLock<T>(calendarId: string | undefined, run: () => Promise<T>): Promise<T>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CalendarEventSummary } from './calendar.js';
|
|
2
2
|
/** The pushable subset of an event. `htmlLink` and `status` are read-only on
|
|
3
3
|
* Google's side, so a baseline for them could never be acted on. */
|
|
4
|
-
export type ShadowEvent = Pick<CalendarEventSummary, "summary" | "start" | "end" | "colorId">;
|
|
4
|
+
export type ShadowEvent = Pick<CalendarEventSummary, "summary" | "start" | "end" | "colorId" | "description" | "location">;
|
|
5
5
|
/** `<workspace>/data/calendar/.push-state.json` */
|
|
6
6
|
export declare function calendarPushStatePath(workspaceRoot?: string): string;
|
|
7
7
|
export declare const toShadowEvent: (event: CalendarEventSummary) => ShadowEvent;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
|
|
2
|
+
import { GOOGLE_CALENDAR_SOURCE_FIELDS } from '../collection/core/schemaZ.js';
|
|
3
|
+
import { CalendarEventSummary } from './calendar.js';
|
|
4
|
+
/** The event fields a schema may map from — narrowed to real keys of
|
|
5
|
+
* `CalendarEventSummary` so the projection below needs no cast. */
|
|
6
|
+
export type GoogleCalendarSourceField = (typeof GOOGLE_CALENDAR_SOURCE_FIELDS)[number];
|
|
7
|
+
/** Project one Google event onto the collection's own field names. The
|
|
8
|
+
* primary field always takes the event id — upsert-by-id is what keeps the
|
|
9
|
+
* sync idempotent, so it is deliberately not remappable. */
|
|
10
|
+
export declare function toCollectionRecord(event: CalendarEventSummary, map: Record<string, GoogleCalendarSourceField>, primaryKey: string, fields: Record<string, CollectionFieldSpec>): CollectionItem;
|
|
11
|
+
/** Google's mapped values laid over whatever the record already holds.
|
|
12
|
+
*
|
|
13
|
+
* A record file is written whole (`writeItem`), so projecting alone would drop
|
|
14
|
+
* every column the map does not name — a collection cannot then carry a local
|
|
15
|
+
* note next to a mirrored event, because the next pull that touches that event
|
|
16
|
+
* silently deletes it. The projection still wins on the fields it covers: those
|
|
17
|
+
* are Google's to own. */
|
|
18
|
+
export declare function mergeIntoExisting(existing: CollectionItem | null, projected: CollectionItem): CollectionItem;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { LoadedCollection } from '../collection/server/discoveredCollection.js';
|
|
2
|
+
import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
|
|
3
|
+
import { CalendarEventSummary } from './calendar.js';
|
|
4
|
+
import { ShadowEvent } from './calendarPushState.js';
|
|
5
|
+
import { PushableSourceField } from './pushPlan.js';
|
|
2
6
|
/** What the up-front writability gate knows about the target calendar. */
|
|
3
7
|
export interface CalendarWriteTarget {
|
|
4
8
|
/** The role from the user's calendar LIST, or null when the calendar is not in
|
|
@@ -27,6 +31,15 @@ export interface CalendarCollectionPushResult {
|
|
|
27
31
|
/** Records that cannot be pushed as they stand, each with the reason. */
|
|
28
32
|
skipped: string[];
|
|
29
33
|
errors: string[];
|
|
34
|
+
/** Records whose local edit did NOT reach Google and would be destroyed by
|
|
35
|
+
* the pull that follows an automatic push (#2620).
|
|
36
|
+
*
|
|
37
|
+
* Only the two states where overwriting loses work with nowhere to recover
|
|
38
|
+
* it from: a both-sides conflict, and an unexpected failure. A `skipped`
|
|
39
|
+
* record is deliberately absent — several of its reasons (an id already
|
|
40
|
+
* taken in Google, an unmappable span) are recovered BY letting the pull
|
|
41
|
+
* write, so protecting them would stall the collection instead. */
|
|
42
|
+
unpushedIds: string[];
|
|
30
43
|
}
|
|
31
44
|
export type CalendarPushOutcome = {
|
|
32
45
|
kind: "pushed";
|
|
@@ -55,6 +68,54 @@ export interface CalendarPushDeps {
|
|
|
55
68
|
accessToken: () => Promise<string>;
|
|
56
69
|
calendarMeta: (accessToken: string, calendarId: string | undefined) => Promise<CalendarWriteTarget>;
|
|
57
70
|
}
|
|
71
|
+
type PushOutcome = {
|
|
72
|
+
kind: "created";
|
|
73
|
+
event: CalendarEventSummary;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "updated";
|
|
76
|
+
event: CalendarEventSummary;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "conflict";
|
|
79
|
+
}
|
|
80
|
+
/** `event` present only when this record's baseline had to be repaired (an
|
|
81
|
+
* event that already existed in Google), so the write path can persist it. */
|
|
82
|
+
| {
|
|
83
|
+
kind: "unchanged";
|
|
84
|
+
event?: CalendarEventSummary;
|
|
85
|
+
} | {
|
|
86
|
+
kind: "skipped";
|
|
87
|
+
message: string;
|
|
88
|
+
} | {
|
|
89
|
+
kind: "error";
|
|
90
|
+
message: string;
|
|
91
|
+
};
|
|
92
|
+
export type PushOutcomeKind = PushOutcome["kind"];
|
|
93
|
+
/** Whether this outcome leaves a local edit that only exists locally, so the
|
|
94
|
+
* pull must not overwrite the record and the baseline must not advance. */
|
|
95
|
+
export declare const isUnpushed: (kind: PushOutcomeKind) => boolean;
|
|
96
|
+
/** Record ids whose local value no longer matches the baseline — edits that have
|
|
97
|
+
* not reached Google.
|
|
98
|
+
*
|
|
99
|
+
* Pure, and deliberately built from `planRecord`, so it answers with exactly the
|
|
100
|
+
* records a push WOULD have acted on. A private re-implementation would drift
|
|
101
|
+
* from the push and protect the wrong set. */
|
|
102
|
+
export declare function locallyEditedIds(records: readonly CollectionItem[], shadow: Record<string, ShadowEvent>, map: Record<string, PushableSourceField>, primaryKey: string, fields: Record<string, CollectionFieldSpec>): string[];
|
|
103
|
+
/** The same set, read from the workspace.
|
|
104
|
+
*
|
|
105
|
+
* Needed when a push cannot run AT ALL — a calendar whose role degraded to
|
|
106
|
+
* reader, a revoked grant, an IO failure. The pull still runs, since reading
|
|
107
|
+
* needs no write access, and would overwrite exactly these records while
|
|
108
|
+
* advancing their baseline past the edit (CodeRabbit review #2666). Computed
|
|
109
|
+
* from the stored baseline alone, so it works precisely when Google is the
|
|
110
|
+
* thing that is unreachable. */
|
|
111
|
+
export declare function unsentLocalEdits(collection: LoadedCollection, workspaceRoot: string): Promise<string[]>;
|
|
112
|
+
/** Push one collection WITHOUT taking the calendar lock.
|
|
113
|
+
*
|
|
114
|
+
* The caller must already hold it. The scheduled push→pull cycle
|
|
115
|
+
* (`collectionSync.ts`) runs inside the lock it took for the pull, and the lock
|
|
116
|
+
* is not reentrant — going through `pushCalendarForCollection` there would make
|
|
117
|
+
* the sync wait on itself forever. */
|
|
118
|
+
export declare function pushCollectionNow(collection: LoadedCollection, workspaceRoot: string, deps?: CalendarPushDeps): Promise<CalendarPushOutcome>;
|
|
58
119
|
/** Push ONE collection's records to the calendar it declares.
|
|
59
120
|
*
|
|
60
121
|
* "Does this collection sync at all" is answered before "is Google linked", for
|
|
@@ -62,3 +123,4 @@ export interface CalendarPushDeps {
|
|
|
62
123
|
* account for a collection that never declared a calendar sends them fixing the
|
|
63
124
|
* wrong thing. */
|
|
64
125
|
export declare function pushCalendarForCollection(slug: string, workspaceRoot: string, deps?: CalendarPushDeps): Promise<CalendarPushOutcome>;
|
|
126
|
+
export {};
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { SystemTaskDef } from '../scheduler/adapter.js';
|
|
2
2
|
import { LoadedCollection } from '../collection/server/discoveredCollection.js';
|
|
3
3
|
import { DeleteItemResult, WriteItemResult } from '../collection/server/io.js';
|
|
4
|
-
import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
|
|
5
|
-
import { GOOGLE_CALENDAR_SOURCE_FIELDS } from '../collection/core/schemaZ.js';
|
|
6
4
|
import { CalendarEventSummary } from './calendar.js';
|
|
7
5
|
import { ShadowEvent } from './calendarPushState.js';
|
|
8
6
|
export declare const GOOGLE_CALENDAR_SYNC_TASK_ID = "system:google-calendar-sync";
|
|
@@ -16,13 +14,6 @@ export interface CalendarCollectionSyncResult {
|
|
|
16
14
|
/** Retryable failures. Any of these hold the sync token back. */
|
|
17
15
|
errors: string[];
|
|
18
16
|
}
|
|
19
|
-
/** The event fields a schema may map from — narrowed to real keys of
|
|
20
|
-
* `CalendarEventSummary` so the projection below needs no cast. */
|
|
21
|
-
type GoogleCalendarSourceField = (typeof GOOGLE_CALENDAR_SOURCE_FIELDS)[number];
|
|
22
|
-
/** Project one Google event onto the collection's own field names. The
|
|
23
|
-
* primary field always takes the event id — upsert-by-id is what keeps the
|
|
24
|
-
* sync idempotent, so it is deliberately not remappable. */
|
|
25
|
-
export declare function toCollectionRecord(event: CalendarEventSummary, map: Record<string, GoogleCalendarSourceField>, primaryKey: string, fields: Record<string, CollectionFieldSpec>): CollectionItem;
|
|
26
17
|
/** `skipped` is a benign no-op; `unwritable` can never succeed so it must NOT
|
|
27
18
|
* hold the token; `error` is retryable and does hold it. */
|
|
28
19
|
type ApplyOutcome = {
|
|
@@ -40,13 +31,49 @@ type ApplyOutcome = {
|
|
|
40
31
|
};
|
|
41
32
|
export declare function classifyWrite(eventId: string, kind: WriteItemResult["kind"]): ApplyOutcome;
|
|
42
33
|
export declare function classifyDelete(eventId: string, kind: DeleteItemResult["kind"]): ApplyOutcome;
|
|
43
|
-
/**
|
|
34
|
+
/** The events of a window a pull may act on.
|
|
44
35
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
36
|
+
* A record the push just refused to send is edited on BOTH sides. Writing
|
|
37
|
+
* Google's value over it destroys the local edit the push declined to resolve,
|
|
38
|
+
* so the local one stands. Single-sourced because the record write and the
|
|
39
|
+
* baseline write must agree exactly on which events they skip — disagreeing is
|
|
40
|
+
* what would silently overwrite Google on the next push (#2620). */
|
|
41
|
+
export declare function pullableEvents(events: readonly CalendarEventSummary[], unpushed: ReadonlySet<string>): CalendarEventSummary[];
|
|
42
|
+
/** What an automatic push protected, per collection slug. `null` means it could
|
|
43
|
+
* not be worked out at all — see `PROTECTION_UNKNOWN`. */
|
|
44
|
+
export type UnpushedBySlug = ReadonlyMap<string, ReadonlySet<string> | null>;
|
|
45
|
+
/** A collection whose protection is unknown must not be pulled this run.
|
|
46
|
+
*
|
|
47
|
+
* Reported through the retryable `errors` channel rather than as a special case:
|
|
48
|
+
* that already holds the sync token AND skips the baseline save, so the window
|
|
49
|
+
* simply replays next run with nothing lost. Failing OPEN here — pulling with no
|
|
50
|
+
* protection — would overwrite the very edits this exists to protect; the read
|
|
51
|
+
* that failed is no evidence that the pull's own writes would fail too, so they
|
|
52
|
+
* would land (CodeRabbit review #2666). */
|
|
53
|
+
export declare const PROTECTION_UNKNOWN = "could not work out which records to protect after a failed push";
|
|
54
|
+
/** What ONE collection's pull must leave alone: only what ITS OWN push failed to
|
|
55
|
+
* send.
|
|
56
|
+
*
|
|
57
|
+
* Scoped per collection because a calendar can back several of them, and a
|
|
58
|
+
* conflict in one says nothing about the others. Sharing one set across the
|
|
59
|
+
* group starved a collection that never even declares `autoPush`: it cannot
|
|
60
|
+
* conflict, yet a neighbour's conflict froze its records — and the sync token
|
|
61
|
+
* still advanced, so Google never resent them (Codex review #2666). */
|
|
62
|
+
export declare const unpushedFor: (unpushed: UnpushedBySlug, slug: string) => ReadonlySet<string> | null;
|
|
63
|
+
/** What the calendar's BASELINE must leave alone: the union over every
|
|
64
|
+
* collection.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately not per collection, unlike the records above. `.push-state.json`
|
|
67
|
+
* holds ONE baseline per calendar, shared by every collection on it, so there is
|
|
68
|
+
* no per-collection baseline to hold back. Advancing it while any collection
|
|
69
|
+
* still has an unresolved conflict is the failure that silently overwrites
|
|
70
|
+
* Google on the next push, and holding it back only ever means "keep reporting
|
|
71
|
+
* the conflict" — so the union is the safe side of an asymmetry the shared
|
|
72
|
+
* storage forces.
|
|
73
|
+
*
|
|
74
|
+
* A `null` (unknown) entry contributes nothing, because that collection reports
|
|
75
|
+
* a retryable error instead — which stops the baseline being saved at all. */
|
|
76
|
+
export declare const allUnpushed: (unpushed: UnpushedBySlug) => ReadonlySet<string>;
|
|
50
77
|
/** Sync ONE calendar and fan its events out to every collection bound to it.
|
|
51
78
|
*
|
|
52
79
|
* The fan-out is not an optimisation, it is correctness: the sync token is
|
|
@@ -62,16 +89,17 @@ export declare function withKeyedLock<T>(locks: Map<string, Promise<unknown>>, k
|
|
|
62
89
|
* writes are upserts by event id — but it is a wasted full walk. Queued, the
|
|
63
90
|
* second pass resumes from the token the first just stored and fetches only
|
|
64
91
|
* what is genuinely new. */
|
|
65
|
-
/** Serialise anything that touches ONE calendar's sync state.
|
|
66
|
-
*
|
|
67
|
-
* Shared with the push path (#2598), not only the pull doors: a push that
|
|
68
|
-
* overtakes an in-flight pull would record a baseline for events the pull is
|
|
69
|
-
* still writing, so the next push would diff against a future it never saw. */
|
|
70
|
-
export declare function withCalendarLock<T>(calendarId: string | undefined, run: () => Promise<T>): Promise<T>;
|
|
71
92
|
export declare function syncCalendarGroup(calendarId: string | undefined, collections: readonly LoadedCollection[], workspaceRoot: string): Promise<CalendarCollectionSyncResult[]>;
|
|
72
93
|
/** The baseline this window establishes: what Google now says per event, and
|
|
73
|
-
* `null` for a cancelled one so a recreate cannot resume from a dead baseline.
|
|
74
|
-
|
|
94
|
+
* `null` for a cancelled one so a recreate cannot resume from a dead baseline.
|
|
95
|
+
*
|
|
96
|
+
* An event whose record the push could not send is left OUT, and that omission
|
|
97
|
+
* is load-bearing. Advancing its baseline to Google's new value while the record
|
|
98
|
+
* keeps the local one would make the next push read a plain one-sided edit —
|
|
99
|
+
* no conflict to detect any more — and quietly overwrite Google. Held back, the
|
|
100
|
+
* baseline stays older than both sides, so the conflict keeps being reported
|
|
101
|
+
* until someone resolves it (#2620). */
|
|
102
|
+
export declare function shadowUpdates(events: readonly CalendarEventSummary[], unpushed?: ReadonlySet<string>): Record<string, ShadowEvent | null>;
|
|
75
103
|
/** Liveness of the collections a sync just wrote to, checked against the skill
|
|
76
104
|
* dir `deleteCollection` removes. `exists` is injected so the rule is testable
|
|
77
105
|
* without a filesystem. An empty group has no survivor by definition. */
|