@thomasfosterau/pulpit 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +1271 -1
- package/dist/blobs.d.ts +249 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +193 -0
- package/dist/blobs.js.map +1 -0
- package/dist/budgets.d.ts +255 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +211 -0
- package/dist/budgets.js.map +1 -0
- package/dist/cache.d.ts +154 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +118 -0
- package/dist/cache.js.map +1 -0
- package/dist/collections.d.ts +242 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/comments.d.ts +212 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +192 -0
- package/dist/comments.js.map +1 -0
- package/dist/content.d.ts +231 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +204 -0
- package/dist/content.js.map +1 -0
- package/dist/documents.d.ts +184 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +190 -0
- package/dist/documents.js.map +1 -0
- package/dist/edges.d.ts +645 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +558 -0
- package/dist/edges.js.map +1 -0
- package/dist/filter.d.ts +461 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +387 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/filter-columns.d.ts +8 -0
- package/dist/internal/filter-columns.d.ts.map +1 -0
- package/dist/internal/filter-columns.js +43 -0
- package/dist/internal/filter-columns.js.map +1 -0
- package/dist/internal/list-order.d.ts +26 -0
- package/dist/internal/list-order.d.ts.map +1 -0
- package/dist/internal/list-order.js +92 -0
- package/dist/internal/list-order.js.map +1 -0
- package/dist/internal/owned-columns.d.ts +93 -0
- package/dist/internal/owned-columns.d.ts.map +1 -0
- package/dist/internal/owned-columns.js +98 -0
- package/dist/internal/owned-columns.js.map +1 -0
- package/dist/internal/plain-rows.d.ts +32 -0
- package/dist/internal/plain-rows.d.ts.map +1 -0
- package/dist/internal/plain-rows.js +32 -0
- package/dist/internal/plain-rows.js.map +1 -0
- package/dist/internal/reference-columns.d.ts +80 -0
- package/dist/internal/reference-columns.d.ts.map +1 -0
- package/dist/internal/reference-columns.js +102 -0
- package/dist/internal/reference-columns.js.map +1 -0
- package/dist/moderation.d.ts +392 -0
- package/dist/moderation.d.ts.map +1 -0
- package/dist/moderation.js +375 -0
- package/dist/moderation.js.map +1 -0
- package/dist/paths.d.ts +204 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +165 -0
- package/dist/paths.js.map +1 -0
- package/dist/proposals.d.ts +523 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +505 -0
- package/dist/proposals.js.map +1 -0
- package/dist/publication.d.ts +148 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +183 -0
- package/dist/publication.js.map +1 -0
- package/dist/purge.d.ts +224 -0
- package/dist/purge.d.ts.map +1 -0
- package/dist/purge.js +151 -0
- package/dist/purge.js.map +1 -0
- package/dist/reactions.d.ts +246 -0
- package/dist/reactions.d.ts.map +1 -0
- package/dist/reactions.js +183 -0
- package/dist/reactions.js.map +1 -0
- package/dist/registry.d.ts +316 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +305 -0
- package/dist/registry.js.map +1 -0
- package/dist/search.d.ts +366 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +273 -0
- package/dist/search.js.map +1 -0
- package/dist/social.d.ts +311 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +205 -0
- package/dist/social.js.map +1 -0
- package/dist/subtype.d.ts +1613 -0
- package/dist/subtype.d.ts.map +1 -0
- package/dist/subtype.js +1300 -0
- package/dist/subtype.js.map +1 -0
- package/dist/testing.d.ts +675 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1022 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-chain.d.ts +702 -0
- package/dist/version-chain.d.ts.map +1 -0
- package/dist/version-chain.js +489 -0
- package/dist/version-chain.js.map +1 -0
- package/dist/working-copy.d.ts +116 -0
- package/dist/working-copy.d.ts.map +1 -0
- package/dist/working-copy.js +95 -0
- package/dist/working-copy.js.map +1 -0
- package/package.json +138 -4
- package/src/blobs.ts +384 -0
- package/src/budgets.ts +338 -0
- package/src/cache.ts +218 -0
- package/src/collections.ts +427 -0
- package/src/comments.ts +392 -0
- package/src/content.ts +238 -0
- package/src/documents.ts +284 -0
- package/src/edges.ts +1185 -0
- package/src/filter.ts +523 -0
- package/src/index.ts +153 -0
- package/src/internal/filter-columns.ts +50 -0
- package/src/internal/list-order.ts +117 -0
- package/src/internal/owned-columns.ts +140 -0
- package/src/internal/plain-rows.ts +54 -0
- package/src/internal/reference-columns.ts +156 -0
- package/src/moderation.ts +728 -0
- package/src/paths.ts +267 -0
- package/src/proposals.ts +1014 -0
- package/src/publication.ts +225 -0
- package/src/purge.ts +313 -0
- package/src/reactions.ts +347 -0
- package/src/registry.ts +524 -0
- package/src/search.ts +540 -0
- package/src/social.ts +469 -0
- package/src/subtype.ts +3095 -0
- package/src/testing.ts +1873 -0
- package/src/version-chain.ts +1093 -0
- package/src/working-copy.ts +192 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The moderation workflow** — the rules that move a leaf's row between
|
|
3
|
+
* moderation states, the threshold grading of a classifier's scores, the
|
|
4
|
+
* review-queue filter, and member reports.
|
|
5
|
+
*
|
|
6
|
+
* The behavioural spec is gardeneria's comment moderation
|
|
7
|
+
* (`apps/api/src/operations/comments.ts`' `moderateCommentE`), its moderation
|
|
8
|
+
* plane (`apps/api/src/operations/moderation.ts`) and its threshold grader
|
|
9
|
+
* (`packages/jobs/src/moderation/thresholds.ts`), with their tests; a
|
|
10
|
+
* `#NNN` here names a gardeneria issue.
|
|
11
|
+
*
|
|
12
|
+
* ## The model (owner decisions on pulpit#51)
|
|
13
|
+
*
|
|
14
|
+
* The kernel owns the rules; the transport (a queue, the classifier run, the
|
|
15
|
+
* API-call ledger, moderator notifications) stays the application's.
|
|
16
|
+
*
|
|
17
|
+
* - **The state lives on the version row**, in the leaf's
|
|
18
|
+
* `moderationState` column ({@link ModerationState}): a decision is an
|
|
19
|
+
* appended version, so history shows who decided what, and when.
|
|
20
|
+
* - **Transitions.** Every new row starts `pending`. Only the moderate rung
|
|
21
|
+
* changes the state: `pending → approved | rejected`, re-moderation
|
|
22
|
+
* between `approved` and `rejected`, and re-review back to `pending`
|
|
23
|
+
* ({@link isModerationTransition}). A write by anyone else that changes the
|
|
24
|
+
* state fails `WriteRejected`.
|
|
25
|
+
* - **Re-review triggers are the leaf's**: an edit that changes one of its
|
|
26
|
+
* declared `reReviewOn` attributes sends an `approved` row back to
|
|
27
|
+
* `pending` ({@link ModerationDeclaration}).
|
|
28
|
+
* - **Owners withdraw their own content** by deleting it: the owned write
|
|
29
|
+
* gate admits its owner in every moderation state, and a tombstone needs
|
|
30
|
+
* no decision.
|
|
31
|
+
*
|
|
32
|
+
* {@link moderationHook} enforces the rules as a before hook (`./subtype`'s
|
|
33
|
+
* lifecycle seam), so every write path honours them, and
|
|
34
|
+
* {@link defineModeration}'s `moderate` is the decision itself.
|
|
35
|
+
*
|
|
36
|
+
* ## Grading and the queue
|
|
37
|
+
*
|
|
38
|
+
* {@link gradeScores} turns a classifier's per-category scores (shaped like
|
|
39
|
+
* effect-ai-ext's `ModerationModel` output; pulpit doesn't import it) into a
|
|
40
|
+
* verdict against the application's thresholds. Classification itself runs
|
|
41
|
+
* from an application's after hook. The review queue is the leaf's pending
|
|
42
|
+
* rows ({@link reviewQueueFilter}) plus the open member reports
|
|
43
|
+
* ({@link defineReports}), both the moderate rung's alone.
|
|
44
|
+
*
|
|
45
|
+
* @copyright Thomas Foster
|
|
46
|
+
* @module pulpit/moderation
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
import * as Arr from "effect/Array";
|
|
50
|
+
import * as Effect from "effect/Effect";
|
|
51
|
+
import * as Equal from "effect/Equal";
|
|
52
|
+
import * as Formatter from "effect/Formatter";
|
|
53
|
+
import * as Option from "effect/Option";
|
|
54
|
+
import * as Order from "effect/Order";
|
|
55
|
+
import * as Predicate from "effect/Predicate";
|
|
56
|
+
import * as Record from "effect/Record";
|
|
57
|
+
import * as Result from "effect/Result";
|
|
58
|
+
import * as Schema from "effect/Schema";
|
|
59
|
+
import { always, eq, never } from "./filter.js";
|
|
60
|
+
import { nowDate, plainRowQuery, signedIn as signedInAs } from "./internal/plain-rows.js";
|
|
61
|
+
import { ModerationState } from "./publication.js";
|
|
62
|
+
import { Forbidden, Unauthorized, WriteRejected, } from "./subtype.js";
|
|
63
|
+
// ── Transitions ───────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Whether a row may move from `from` (none for a row being created) to
|
|
66
|
+
* `to`: a new row only to `pending`; an existing one to any other state
|
|
67
|
+
* (decision, re-moderation, re-review). Staying put is no transition.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { strictEqual } from "node:assert";
|
|
72
|
+
* import * as Option from "effect/Option";
|
|
73
|
+
* import { isModerationTransition } from "@thomasfosterau/pulpit/moderation";
|
|
74
|
+
*
|
|
75
|
+
* strictEqual(isModerationTransition(Option.none(), "pending"), true);
|
|
76
|
+
* strictEqual(isModerationTransition(Option.none(), "approved"), false);
|
|
77
|
+
* strictEqual(isModerationTransition(Option.some("pending"), "approved"), true);
|
|
78
|
+
* strictEqual(isModerationTransition(Option.some("approved"), "approved"), false);
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export const isModerationTransition = (from, to) => Option.match(from, { onNone: () => to === "pending", onSome: (state) => state !== to });
|
|
84
|
+
/**
|
|
85
|
+
* The declaration with the conventional column names.
|
|
86
|
+
*
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
*/
|
|
89
|
+
export const moderatedColumns = (reReviewOn) => ({
|
|
90
|
+
column: "moderationState",
|
|
91
|
+
moderatedAtColumn: Option.some("moderatedAt"),
|
|
92
|
+
moderatedByColumn: Option.some("moderatedBy"),
|
|
93
|
+
reReviewOn,
|
|
94
|
+
});
|
|
95
|
+
const isModerationState = Schema.is(ModerationState);
|
|
96
|
+
/** A column's value in a state or patch, absent (or `undefined`) as none. */
|
|
97
|
+
const valueOf = (values, column) => Option.fromUndefinedOr(values[column]);
|
|
98
|
+
/** The prior state's moderation state, or none (a create, or a value outside the vocabulary). */
|
|
99
|
+
const priorState = (event, column) => Option.flatMap(event.prior, (prior) => Option.filter(valueOf(prior.state, column), isModerationState));
|
|
100
|
+
/** The decision stamps for a patch, by the leaf's declared columns. */
|
|
101
|
+
const stamps = (declaration, account, now) => ({
|
|
102
|
+
...Option.match(declaration.moderatedAtColumn, {
|
|
103
|
+
onNone: () => ({}),
|
|
104
|
+
onSome: (column) => Record.singleton(column, now),
|
|
105
|
+
}),
|
|
106
|
+
...Option.match(declaration.moderatedByColumn, {
|
|
107
|
+
onNone: () => ({}),
|
|
108
|
+
onSome: (column) => Record.singleton(column, account.id),
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
/** The prior row's moderation columns, carried over a restored version's. */
|
|
112
|
+
const carried = (event, declaration) => Option.match(event.prior, {
|
|
113
|
+
onNone: () => ({}),
|
|
114
|
+
onSome: (prior) => Record.fromEntries(Arr.map([
|
|
115
|
+
declaration.column,
|
|
116
|
+
...Option.toArray(declaration.moderatedAtColumn),
|
|
117
|
+
...Option.toArray(declaration.moderatedByColumn),
|
|
118
|
+
], (column) => [column, prior.state[column] ?? null])),
|
|
119
|
+
});
|
|
120
|
+
/** Whether the patch changes a re-review attribute from its prior value. */
|
|
121
|
+
const touchesReview = (event, declaration) => Arr.some(declaration.reReviewOn, (column) => Option.match(valueOf(event.patch, column), {
|
|
122
|
+
onNone: () => false,
|
|
123
|
+
onSome: (next) => !Option.exists(event.prior, (prior) => Option.exists(valueOf(prior.state, column), (before) => Equal.equals(before, next))),
|
|
124
|
+
}));
|
|
125
|
+
/**
|
|
126
|
+
* The moderation rules applied to one write: the patch the write should
|
|
127
|
+
* apply, or the reason it is refused. Pure, so every case is testable
|
|
128
|
+
* without a chain.
|
|
129
|
+
*
|
|
130
|
+
* - A **create** stores `pending`; a patch asking for another state is refused.
|
|
131
|
+
* - A **delete** is untouched (withdrawal needs no decision).
|
|
132
|
+
* - A **`restoreVersion`** by anyone but the moderate rung carries the
|
|
133
|
+
* prior row's moderation columns over the restored version's, so an old
|
|
134
|
+
* approval never comes back with old content.
|
|
135
|
+
* - A patch that **changes the state** is a decision: refused below the
|
|
136
|
+
* moderate rung or outside {@link isModerationTransition}, and otherwise
|
|
137
|
+
* stamped (`moderatedAtColumn`, `moderatedByColumn`). A patch that sets
|
|
138
|
+
* the state it already has changes nothing.
|
|
139
|
+
* - Otherwise, an edit that changes a `reReviewOn` attribute of an
|
|
140
|
+
* `approved` row sends it back to `pending`.
|
|
141
|
+
*
|
|
142
|
+
* @since 0.1.0
|
|
143
|
+
*/
|
|
144
|
+
export const moderatePatch = (event, declaration, policy, now) => {
|
|
145
|
+
const moderator = Option.filter(event.viewer, policy.canModerate);
|
|
146
|
+
if (event.action === "delete")
|
|
147
|
+
return Result.succeed(event.patch);
|
|
148
|
+
if (event.action === "create")
|
|
149
|
+
return created(event.patch, declaration.column);
|
|
150
|
+
const patch = event.action === "restoreVersion" && Option.isNone(moderator)
|
|
151
|
+
? { ...event.patch, ...carried(event, declaration) }
|
|
152
|
+
: event.patch;
|
|
153
|
+
const from = priorState(event, declaration.column);
|
|
154
|
+
const asked = Option.filter(valueOf(patch, declaration.column), (value) => !Option.exists(from, (state) => state === value));
|
|
155
|
+
return Option.isSome(asked)
|
|
156
|
+
? decided({ event, patch, from, asked: asked.value, moderator, declaration, now })
|
|
157
|
+
: Result.succeed(reReviewed({ ...event, patch }, from, declaration));
|
|
158
|
+
};
|
|
159
|
+
/** A create's patch: `pending`, whatever it asked for, unless it asked for another state. */
|
|
160
|
+
const created = (patch, column) => Option.exists(valueOf(patch, column), (asked) => asked !== "pending")
|
|
161
|
+
? Result.fail(`a new row starts pending; ${column} is the moderate rung's to change`)
|
|
162
|
+
: Result.succeed({ ...patch, [column]: "pending" });
|
|
163
|
+
/** A patch that changes the state: the rung's decision, stamped (a restored version unstamped). */
|
|
164
|
+
const decided = (decision) => {
|
|
165
|
+
const { patch, asked, moderator, declaration } = decision;
|
|
166
|
+
const { column } = declaration;
|
|
167
|
+
if (Option.isNone(moderator))
|
|
168
|
+
return Result.fail(`${column} is the moderate rung's to change`);
|
|
169
|
+
if (!isModerationState(asked) || !isModerationTransition(decision.from, asked)) {
|
|
170
|
+
return Result.fail(`${column} can't move to ${Predicate.isString(asked) ? asked : Formatter.format(asked)}`);
|
|
171
|
+
}
|
|
172
|
+
return Result.succeed(decision.event.action === "restoreVersion"
|
|
173
|
+
? patch
|
|
174
|
+
: { ...patch, ...stamps(declaration, moderator.value, decision.now) });
|
|
175
|
+
};
|
|
176
|
+
/** An edit that leaves the state alone: back to `pending` when it touches a re-review attribute of an approved row. */
|
|
177
|
+
const reReviewed = (event, from, declaration) => Option.exists(from, (state) => state === "approved") && touchesReview(event, declaration)
|
|
178
|
+
? { ...event.patch, [declaration.column]: "pending" }
|
|
179
|
+
: event.patch;
|
|
180
|
+
/**
|
|
181
|
+
* The moderation rules ({@link moderatePatch}) as a before hook for the
|
|
182
|
+
* leaf's `LifecycleHooks`: every write applies them, and a refusal
|
|
183
|
+
* fails the write with `WriteRejected`, appending nothing. Compose it first,
|
|
184
|
+
* so later hooks see the state it settled.
|
|
185
|
+
*
|
|
186
|
+
* @since 0.1.0
|
|
187
|
+
*/
|
|
188
|
+
export const moderationHook = (declaration, policy) => ({
|
|
189
|
+
name: "moderation",
|
|
190
|
+
run: (event) => Effect.flatMap(nowDate, (now) => {
|
|
191
|
+
return Result.match(moderatePatch(event, declaration, policy, now), {
|
|
192
|
+
onSuccess: Effect.succeed,
|
|
193
|
+
onFailure: (reason) => Effect.fail(new WriteRejected({ type: event.type, id: event.id, action: event.action, reason })),
|
|
194
|
+
});
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
/**
|
|
198
|
+
* Bind a leaf's moderation decision. The leaf's hooks must include
|
|
199
|
+
* {@link moderationHook} with the same declaration: it is what admits the
|
|
200
|
+
* decision and stamps it.
|
|
201
|
+
*
|
|
202
|
+
* @since 0.1.0
|
|
203
|
+
*/
|
|
204
|
+
export const defineModeration = (definition) => ({
|
|
205
|
+
moderate: Effect.fn(`${definition.target.type}.moderate`)(function* (id, decision, precondition = {}) {
|
|
206
|
+
const viewer = yield* definition.viewer;
|
|
207
|
+
if (Option.isNone(viewer))
|
|
208
|
+
return yield* new Unauthorized();
|
|
209
|
+
if (!definition.policy.canModerate(viewer.value))
|
|
210
|
+
return yield* new Forbidden();
|
|
211
|
+
return yield* definition.target.update({
|
|
212
|
+
id,
|
|
213
|
+
patch: { [definition.declaration.column]: decision },
|
|
214
|
+
expectedVersion: precondition.expectedVersion,
|
|
215
|
+
});
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
/**
|
|
219
|
+
* The review queue's leaf half as a list filter: the `pending` rows, for
|
|
220
|
+
* the moderate rung only (`never` for anyone else). Pass it as a list
|
|
221
|
+
* request's `where`; the leaf's own read arms still apply.
|
|
222
|
+
*
|
|
223
|
+
* @since 0.1.0
|
|
224
|
+
*/
|
|
225
|
+
export const reviewQueueFilter = (viewer, policy, column = "moderationState") => (Option.exists(viewer, policy.canModerate) ? eq(column, "pending") : never);
|
|
226
|
+
/**
|
|
227
|
+
* The band a flagged row lands in, from its worst breach.
|
|
228
|
+
*
|
|
229
|
+
* @since 0.1.0
|
|
230
|
+
*/
|
|
231
|
+
export const ModerationPriority = Schema.Literals(["low", "medium", "high", "urgent"]);
|
|
232
|
+
/**
|
|
233
|
+
* The bar a category with no configured threshold is graded against;
|
|
234
|
+
* strictly above it flags. A starting point to tune, not a measured value.
|
|
235
|
+
*
|
|
236
|
+
* @since 0.1.0
|
|
237
|
+
*/
|
|
238
|
+
export const DEFAULT_MODERATION_THRESHOLD = 0.5;
|
|
239
|
+
/** The band for a breach: round cut points, a starting policy. */
|
|
240
|
+
const bandFor = (score) => score >= 0.9 ? "urgent" : score >= 0.75 ? "high" : score >= 0.5 ? "medium" : "low";
|
|
241
|
+
/**
|
|
242
|
+
* Grade a classifier's scores against the leaf's thresholds (a category
|
|
243
|
+
* missing from `thresholds` grades against `defaultThreshold`). A category
|
|
244
|
+
* breaches when its score is strictly above its bar; an unassessed one is
|
|
245
|
+
* absent and can't breach. The classifier's own flag is never overridden
|
|
246
|
+
* downward, only added to (raise a category's bar to `1` to ignore it). The
|
|
247
|
+
* band is the worst breaching score's, or the top score's for a flag with
|
|
248
|
+
* no breach. Pure.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import { deepStrictEqual } from "node:assert";
|
|
253
|
+
* import { gradeScores } from "@thomasfosterau/pulpit/moderation";
|
|
254
|
+
*
|
|
255
|
+
* deepStrictEqual(
|
|
256
|
+
* gradeScores({ flagged: false, categoryScores: { hate: 0.8, spam: 0.3 } }, { spam: 0.2 }),
|
|
257
|
+
* { isFlagged: true, exceeded: ["hate", "spam"], topScore: 0.8, priority: "high" },
|
|
258
|
+
* );
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @since 0.1.0
|
|
262
|
+
*/
|
|
263
|
+
export const gradeScores = (scores, thresholds, defaultThreshold = DEFAULT_MODERATION_THRESHOLD) => {
|
|
264
|
+
const entries = Record.toEntries(scores.categoryScores);
|
|
265
|
+
const breaches = Arr.filter(entries, ([category, score]) => score > (thresholds[category] ?? defaultThreshold));
|
|
266
|
+
const top = (values) => Arr.reduce(values, 0, (worst, [, score]) => Math.max(worst, score));
|
|
267
|
+
const topScore = top(entries);
|
|
268
|
+
const topExceeded = top(breaches);
|
|
269
|
+
return {
|
|
270
|
+
isFlagged: scores.flagged || Arr.isReadonlyArrayNonEmpty(breaches),
|
|
271
|
+
exceeded: Arr.sort(Arr.map(breaches, ([category]) => category), Order.String),
|
|
272
|
+
topScore,
|
|
273
|
+
priority: bandFor(topExceeded > 0 ? topExceeded : scores.flagged ? topScore : 0),
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
// ── Member reports ────────────────────────────────────────────────────
|
|
277
|
+
/**
|
|
278
|
+
* A report's status: `open` until the moderate rung resolves it (acted on)
|
|
279
|
+
* or dismisses it.
|
|
280
|
+
*
|
|
281
|
+
* @since 0.1.0
|
|
282
|
+
*/
|
|
283
|
+
export const ReportStatus = Schema.Literals(["open", "resolved", "dismissed"]);
|
|
284
|
+
/**
|
|
285
|
+
* No report at that id the viewer may read: the wire's 404.
|
|
286
|
+
*
|
|
287
|
+
* @since 0.1.0
|
|
288
|
+
*/
|
|
289
|
+
export class ReportNotFound extends Schema.TaggedError()("ReportNotFound", {
|
|
290
|
+
id: Schema.String,
|
|
291
|
+
}) {
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The report is already resolved or dismissed: the wire's 409.
|
|
295
|
+
*
|
|
296
|
+
* @since 0.1.0
|
|
297
|
+
*/
|
|
298
|
+
export class ReportClosed extends Schema.TaggedError()("ReportClosed", {
|
|
299
|
+
id: Schema.String,
|
|
300
|
+
status: ReportStatus,
|
|
301
|
+
}) {
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Bind member reports against one leaf: filed by a signed-in account that
|
|
305
|
+
* can read the target, read by their reporter and the moderate rung, and
|
|
306
|
+
* resolved by the moderate rung.
|
|
307
|
+
*
|
|
308
|
+
* @since 0.1.0
|
|
309
|
+
*/
|
|
310
|
+
export const defineReports = (definition) => {
|
|
311
|
+
const { target, reports, policy } = definition;
|
|
312
|
+
const type = target.type;
|
|
313
|
+
const signedIn = signedInAs(definition.viewer);
|
|
314
|
+
const file = Effect.fn(`${type}.reports.file`)(function* (input) {
|
|
315
|
+
const account = yield* signedIn;
|
|
316
|
+
// A target the reporter can't read is missing to them (SEC-AUTH-ENUM-001).
|
|
317
|
+
yield* target.get(input.targetId);
|
|
318
|
+
if (definition.budget !== undefined) {
|
|
319
|
+
yield* definition.budget.spend(Option.some(account), "file", input.targetId);
|
|
320
|
+
}
|
|
321
|
+
const id = yield* reports.mintId;
|
|
322
|
+
yield* reports.insert({
|
|
323
|
+
id,
|
|
324
|
+
targetId: input.targetId,
|
|
325
|
+
targetType: type,
|
|
326
|
+
reporterId: account.id,
|
|
327
|
+
reason: input.reason,
|
|
328
|
+
details: input.details,
|
|
329
|
+
status: "open",
|
|
330
|
+
filedAt: yield* nowDate,
|
|
331
|
+
resolvedAt: Option.none(),
|
|
332
|
+
resolvedBy: Option.none(),
|
|
333
|
+
resolutionNote: Option.none(),
|
|
334
|
+
});
|
|
335
|
+
return { id };
|
|
336
|
+
});
|
|
337
|
+
const readable = (viewer, row) => row.targetType === type &&
|
|
338
|
+
Option.exists(viewer, (account) => policy.canModerate(account) || account.id === row.reporterId);
|
|
339
|
+
const get = Effect.fn(`${type}.reports.get`)(function* (id) {
|
|
340
|
+
const viewer = yield* definition.viewer;
|
|
341
|
+
const row = yield* reports.get(id);
|
|
342
|
+
if (Option.isNone(row) || !readable(viewer, row.value)) {
|
|
343
|
+
return yield* new ReportNotFound({ id });
|
|
344
|
+
}
|
|
345
|
+
return row.value;
|
|
346
|
+
});
|
|
347
|
+
const list = Effect.fn(`${type}.reports.list`)(function* (request) {
|
|
348
|
+
const viewer = yield* definition.viewer;
|
|
349
|
+
const scope = Option.match(viewer, {
|
|
350
|
+
onNone: () => never,
|
|
351
|
+
onSome: (account) => policy.canModerate(account) ? always : eq("reporterId", account.id),
|
|
352
|
+
});
|
|
353
|
+
return yield* reports.list(plainRowQuery(type, scope, request));
|
|
354
|
+
});
|
|
355
|
+
const resolve = Effect.fn(`${type}.reports.resolve`)(function* (id, outcome, note) {
|
|
356
|
+
const account = yield* signedIn;
|
|
357
|
+
if (!policy.canModerate(account))
|
|
358
|
+
return yield* new Forbidden();
|
|
359
|
+
const row = yield* reports.lock(id);
|
|
360
|
+
if (Option.isNone(row) || row.value.targetType !== type) {
|
|
361
|
+
return yield* new ReportNotFound({ id });
|
|
362
|
+
}
|
|
363
|
+
if (row.value.status !== "open") {
|
|
364
|
+
return yield* new ReportClosed({ id, status: row.value.status });
|
|
365
|
+
}
|
|
366
|
+
yield* reports.close(id, {
|
|
367
|
+
status: outcome,
|
|
368
|
+
resolvedAt: yield* nowDate,
|
|
369
|
+
resolvedBy: account.id,
|
|
370
|
+
resolutionNote: note,
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
return { file, get, list, resolve };
|
|
374
|
+
};
|
|
375
|
+
//# sourceMappingURL=moderation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation.js","sourceRoot":"","sources":["../src/moderation.ts"],"names":[],"mappings":"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"}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Routable content** — a leaf's path claims, tied to its version chain,
|
|
3
|
+
* the slug aliases a path change leaves behind, and the gated resolution of
|
|
4
|
+
* a path to a node or a redirect.
|
|
5
|
+
*
|
|
6
|
+
* The behavioural spec is gardeneria's `apps/api/src/operations/pages.ts`
|
|
7
|
+
* and `packages/database/src/tables/pages.ts` (one live page per canonical
|
|
8
|
+
* path, old slugs kept in `page_slugs` and answered with a 301). A `#NNN`
|
|
9
|
+
* here names a gardeneria issue.
|
|
10
|
+
*
|
|
11
|
+
* ## The model (owner decision on pulpit#57)
|
|
12
|
+
*
|
|
13
|
+
* - **A claim lives on the root node**: one path per content item. The
|
|
14
|
+
* locale is chosen when the resolved node is read, never by the path, so a
|
|
15
|
+
* slug translation is not a claim of its own (pulpit#56's model (b)).
|
|
16
|
+
* - **Claims follow the node's lifecycle.** A write that leaves the node
|
|
17
|
+
* live claims the path its new state holds in the leaf's path column; a
|
|
18
|
+
* tombstone, or a state with no path, frees the claim; a restore re-claims
|
|
19
|
+
* its path, or fails {@link PathTaken} when another node holds it now.
|
|
20
|
+
* - **A path change records an alias**: the old path resolves as a redirect
|
|
21
|
+
* to the new one, for as long as no other node claims it.
|
|
22
|
+
* - **Resolution is gated** (`SEC-AUTH-ENUM-001`): {@link RoutableOps.resolve}
|
|
23
|
+
* reads the node through the leaf's own `get`, so a hidden node's path,
|
|
24
|
+
* and an alias to a hidden node, is {@link PathNotFound}, never a redirect.
|
|
25
|
+
*
|
|
26
|
+
* Free-form redirects (patterns, a 404 log) are the site's. The path's
|
|
27
|
+
* syntax is the leaf attribute's schema (effect-schema-ext's `Ids.slug`, once
|
|
28
|
+
* it publishes).
|
|
29
|
+
*
|
|
30
|
+
* @copyright Thomas Foster
|
|
31
|
+
* @module pulpit/paths
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
*/
|
|
34
|
+
import * as Effect from "effect/Effect";
|
|
35
|
+
import * as Option from "effect/Option";
|
|
36
|
+
import * as Schema from "effect/Schema";
|
|
37
|
+
import type { SubtypeOps } from "./subtype.ts";
|
|
38
|
+
import type { VersionChainStore } from "./version-chain.ts";
|
|
39
|
+
declare const PathTaken_base: Schema.Class<PathTaken, Schema.TaggedStruct<"PathTaken", {
|
|
40
|
+
readonly path: Schema.String;
|
|
41
|
+
}>, import("effect/Cause").YieldableError & {
|
|
42
|
+
readonly brand: unique symbol;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Another node holds the path canonically: the wire's 409. It names the
|
|
46
|
+
* path, never the holder, so it says only that the path is taken, as a
|
|
47
|
+
* unique-key conflict does, whoever holds it.
|
|
48
|
+
*
|
|
49
|
+
* @since 0.1.0
|
|
50
|
+
*/
|
|
51
|
+
export declare class PathTaken extends PathTaken_base {
|
|
52
|
+
}
|
|
53
|
+
declare const PathNotFound_base: Schema.Class<PathNotFound, Schema.TaggedStruct<"PathNotFound", {
|
|
54
|
+
readonly path: Schema.String;
|
|
55
|
+
}>, import("effect/Cause").YieldableError & {
|
|
56
|
+
readonly brand: unique symbol;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* No node the viewer can read answers to the path: the wire's 404. A path
|
|
60
|
+
* nobody claims, one whose node is hidden from the viewer, and an alias to a
|
|
61
|
+
* hidden node all answer this, alike (`SEC-AUTH-ENUM-001`).
|
|
62
|
+
*
|
|
63
|
+
* @since 0.1.0
|
|
64
|
+
*/
|
|
65
|
+
export declare class PathNotFound extends PathNotFound_base {
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What a path is stored as: the node it names, and whether it is that
|
|
69
|
+
* node's canonical path or an alias a path change left behind.
|
|
70
|
+
*
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*/
|
|
73
|
+
export interface PathEntry {
|
|
74
|
+
readonly nodeId: string;
|
|
75
|
+
readonly canonical: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The path table of one routable namespace (a collection): each path once,
|
|
79
|
+
* canonical for one node or an alias of one. A store that persists and
|
|
80
|
+
* applies no policy; it runs in the caller's transaction, so a write whose
|
|
81
|
+
* claim fails rolls back with its chain append.
|
|
82
|
+
*
|
|
83
|
+
* @since 0.1.0
|
|
84
|
+
*/
|
|
85
|
+
export interface PathStore<E, R> {
|
|
86
|
+
/** The entry for `path`, canonical or alias, or none. */
|
|
87
|
+
readonly lookup: (path: string) => Effect.Effect<Option.Option<PathEntry>, E, R>;
|
|
88
|
+
/** The node's canonical path, or none. */
|
|
89
|
+
readonly canonicalOf: (nodeId: string) => Effect.Effect<Option.Option<string>, E, R>;
|
|
90
|
+
/**
|
|
91
|
+
* Make `path` the node's canonical path, in one step: fails
|
|
92
|
+
* {@link PathTaken} when another node holds it canonically; otherwise
|
|
93
|
+
* replaces an alias at `path` (the node's own, which a revert promotes, or
|
|
94
|
+
* another node's, which the claim takes over) and turns the node's previous
|
|
95
|
+
* canonical path, if any, into its alias.
|
|
96
|
+
*/
|
|
97
|
+
readonly claim: (nodeId: string, path: string) => Effect.Effect<void, PathTaken | E, R>;
|
|
98
|
+
/** Free the node's canonical path, if it has one; its aliases stay. */
|
|
99
|
+
readonly release: (nodeId: string) => Effect.Effect<void, E, R>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* What a path resolves to for one viewer: the node's resource at its
|
|
103
|
+
* canonical path, or a redirect from an alias to the canonical path.
|
|
104
|
+
*
|
|
105
|
+
* @since 0.1.0
|
|
106
|
+
*/
|
|
107
|
+
export type Resolution<Resource> = {
|
|
108
|
+
readonly kind: "node";
|
|
109
|
+
readonly id: string;
|
|
110
|
+
readonly resource: Resource;
|
|
111
|
+
} | {
|
|
112
|
+
readonly kind: "redirect";
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly path: string;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* A routable leaf's operations: the leaf's own, each write keeping its path
|
|
118
|
+
* claim in step with the state it wrote (and failing {@link PathTaken}), plus
|
|
119
|
+
* the gated {@link resolve}.
|
|
120
|
+
*
|
|
121
|
+
* @since 0.1.0
|
|
122
|
+
*/
|
|
123
|
+
export interface RoutableOps<Resource, E, R> extends SubtypeOps<Resource, E | PathTaken, R> {
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a path for the viewer: its node, through the leaf's own `get`, or
|
|
126
|
+
* a redirect from an alias to the node's canonical path. Fails
|
|
127
|
+
* {@link PathNotFound} for an unclaimed path, a hidden node's path, and an
|
|
128
|
+
* alias to a hidden node, alike, and for an alias whose node has no
|
|
129
|
+
* canonical path now (`SEC-AUTH-ENUM-001`); with the store's `E` otherwise.
|
|
130
|
+
*/
|
|
131
|
+
readonly resolve: (path: string) => Effect.Effect<Resolution<Resource>, PathNotFound | E, R>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* What {@link routable} needs besides the leaf's operations.
|
|
135
|
+
*
|
|
136
|
+
* @since 0.1.0
|
|
137
|
+
*/
|
|
138
|
+
export interface RoutableConfig<E, R> {
|
|
139
|
+
/** The leaf's path column: a string holds the node's path; any other value, or none, claims nothing. */
|
|
140
|
+
readonly column: string;
|
|
141
|
+
/** The routable namespace's path table. */
|
|
142
|
+
readonly paths: PathStore<E, R>;
|
|
143
|
+
/** The leaf's chain store, read (ungated) for the state a write landed at. */
|
|
144
|
+
readonly chain: VersionChainStore<E, R>;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Make a leaf routable (pulpit#57): its writes keep the node's path claim in
|
|
148
|
+
* step with its chain, and {@link RoutableOps.resolve} answers a path through
|
|
149
|
+
* the leaf's own read gate. After each write's append, in the same
|
|
150
|
+
* transaction, the claim follows the state the write landed at: a live state
|
|
151
|
+
* with a path claims it (a change leaves the old path as an alias; another
|
|
152
|
+
* node's claim fails {@link PathTaken}), and a tombstone or a state with no
|
|
153
|
+
* path frees it. A restore into a path another node took meanwhile therefore
|
|
154
|
+
* fails `PathTaken`, and its append rolls back with it. Wrap the result with
|
|
155
|
+
* `./subtype`'s `subtypeHandlers` as the leaf's own operations, so its
|
|
156
|
+
* `transact` covers the claims.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { strictEqual } from "node:assert";
|
|
161
|
+
* import * as Effect from "effect/Effect";
|
|
162
|
+
* import * as Option from "effect/Option";
|
|
163
|
+
* import { routable } from "@thomasfosterau/pulpit/paths";
|
|
164
|
+
* import { defineSubtype, nobodyFollows } from "@thomasfosterau/pulpit/subtype";
|
|
165
|
+
* import {
|
|
166
|
+
* makeInMemoryChainStore,
|
|
167
|
+
* makeInMemoryLeafStore,
|
|
168
|
+
* makeInMemoryPathStore,
|
|
169
|
+
* } from "@thomasfosterau/pulpit/testing";
|
|
170
|
+
* import * as HashSet from "effect/HashSet";
|
|
171
|
+
*
|
|
172
|
+
* const chain = makeInMemoryChainStore();
|
|
173
|
+
* const pages = routable(
|
|
174
|
+
* defineSubtype({
|
|
175
|
+
* type: "pages",
|
|
176
|
+
* access: { kind: "reference" },
|
|
177
|
+
* chain,
|
|
178
|
+
* leaves: makeInMemoryLeafStore(chain, { columns: ["slug"] }),
|
|
179
|
+
* viewer: Effect.succeed(Option.some({ id: "editor", role: "admin" })),
|
|
180
|
+
* policy: {
|
|
181
|
+
* canViewPrivate: () => true,
|
|
182
|
+
* canViewDeleted: () => true,
|
|
183
|
+
* canModerate: () => true,
|
|
184
|
+
* canWriteCorpus: () => true,
|
|
185
|
+
* },
|
|
186
|
+
* followers: nobodyFollows(HashSet.empty<string>()),
|
|
187
|
+
* toResource: (row) => ({ id: row.id, slug: row["slug"] }),
|
|
188
|
+
* }),
|
|
189
|
+
* { column: "slug", paths: makeInMemoryPathStore(), chain },
|
|
190
|
+
* );
|
|
191
|
+
* const program = Effect.gen(function* () {
|
|
192
|
+
* const { id } = yield* pages.create({ id: Option.none(), state: { slug: "about" } });
|
|
193
|
+
* yield* pages.update({ id, patch: { slug: "about-us" } });
|
|
194
|
+
* return yield* pages.resolve("about");
|
|
195
|
+
* });
|
|
196
|
+
* const resolved = Effect.runSync(program);
|
|
197
|
+
* strictEqual(resolved.kind === "redirect" && resolved.path, "about-us");
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* @since 0.1.0
|
|
201
|
+
*/
|
|
202
|
+
export declare const routable: <Resource, E, R>(ops: SubtypeOps<Resource, E, R>, config: RoutableConfig<E, R>) => RoutableOps<Resource, E, R>;
|
|
203
|
+
export {};
|
|
204
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
|