@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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The reference leaf's privacy arm** (`PUB-009`), shared by `./subtype`
|
|
3
|
+
* and `./edges` so a reference row is read one way: as a list filter
|
|
4
|
+
* ({@link referenceListFilter}, behind `referencePrivacyFilter`, the subtype
|
|
5
|
+
* `list` and `./edges`' `endpointFilter`) and as a row predicate
|
|
6
|
+
* ({@link canViewReferenceRow}, behind `defineSubtype`'s `visibleRow` and
|
|
7
|
+
* `./edges`' `canViewEndpoint`). Before pulpit#92 each module spelled the
|
|
8
|
+
* predicate for itself.
|
|
9
|
+
*
|
|
10
|
+
* The row's facts are read by `ownedFacts`, so an owner-like value that is
|
|
11
|
+
* not a string reads as no owner and never reaches the follower seam
|
|
12
|
+
* (pulpit#85, pulpit#100).
|
|
13
|
+
*
|
|
14
|
+
* Internal: not a package entry point (absent from `package.json`
|
|
15
|
+
* `exports`) and excluded from the generated API docs (`docgen.json`).
|
|
16
|
+
* Typed structurally, so it never imports `./subtype`.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
import * as Effect from "effect/Effect";
|
|
21
|
+
import * as Option from "effect/Option";
|
|
22
|
+
import { type ListFilter } from "../filter.ts";
|
|
23
|
+
import type { ColumnValue } from "../version-chain.ts";
|
|
24
|
+
/** The reference access declaration, structurally (`./subtype`'s reference `LeafAccess` fits). */
|
|
25
|
+
interface ReferenceColumns {
|
|
26
|
+
readonly followersOfColumn?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** The parts of `./subtype`'s viewer, policy, follower seam and read scope the arm reads, structurally. */
|
|
29
|
+
interface Account {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
}
|
|
32
|
+
interface PrivateRung<A> {
|
|
33
|
+
readonly canViewPrivate: (account: A) => boolean;
|
|
34
|
+
}
|
|
35
|
+
interface IsFollower<R> {
|
|
36
|
+
readonly isFollower: (followerId: string, ownerId: string) => Effect.Effect<boolean, never, R>;
|
|
37
|
+
}
|
|
38
|
+
type Scope<Following> = {
|
|
39
|
+
readonly kind: "all";
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "public";
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: "member";
|
|
44
|
+
readonly userId: string;
|
|
45
|
+
readonly following: Following;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The reference privacy arm as a list filter: `always` over a view without a
|
|
49
|
+
* `visibility` column or for the view-private rung (`scope.kind === "all"`);
|
|
50
|
+
* otherwise `visibility = 'public'`, and for a signed-in viewer also
|
|
51
|
+
* `visibility = 'followers' AND (<followersOfColumn> = <viewer> OR
|
|
52
|
+
* <followersOfColumn> IN (<following>))` when the leaf declares that column
|
|
53
|
+
* and the view carries it (pulpit#92): the account the column names and its
|
|
54
|
+
* followers read its `followers` rows. Its `private` rows stay the
|
|
55
|
+
* view-private rung's alone. Never throws.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export declare const referenceListFilter: <Following>(access: ReferenceColumns, viewColumns: ReadonlySet<string>, scope: Scope<Following>) => ListFilter<Following>;
|
|
60
|
+
/**
|
|
61
|
+
* The reference privacy arm as a row predicate, the same facts as
|
|
62
|
+
* {@link referenceListFilter}: a view without a `visibility` column shows
|
|
63
|
+
* the row; otherwise it shows when its visibility is exactly `public`, to
|
|
64
|
+
* the view-private rung, or, when it is exactly `followers` and the leaf
|
|
65
|
+
* declares a followers column the view carries, to the signed-in account
|
|
66
|
+
* that column names and to its followers, as the follower seam answers
|
|
67
|
+
* (pulpit#92). That account's `private` rows stay the view-private rung's
|
|
68
|
+
* alone. A column value that is not a string names no account, so it never
|
|
69
|
+
* matches the viewer and the seam is never asked about it (pulpit#85,
|
|
70
|
+
* pulpit#100). Anything else is hidden, which the
|
|
71
|
+
* caller answers as `NodeNotFound` (`SEC-AUTH-ENUM-001`). The soft-delete
|
|
72
|
+
* axis is the caller's. Never fails.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export declare const canViewReferenceRow: <A extends Account, R>(access: ReferenceColumns, viewColumns: ReadonlySet<string>, viewer: Option.Option<A>, policy: PrivateRung<A>, followers: IsFollower<R>, row: {
|
|
77
|
+
readonly [column: string]: ColumnValue;
|
|
78
|
+
}) => Effect.Effect<boolean, never, R>;
|
|
79
|
+
export {};
|
|
80
|
+
//# sourceMappingURL=reference-columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-columns.d.ts","sourceRoot":"","sources":["../../src/internal/reference-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,kGAAkG;AAClG,UAAU,gBAAgB;IAExB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjD;AAED,2GAA2G;AAC3G,UAAU,OAAO;IACf,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AACD,UAAU,WAAW,CAAC,CAAC;IACrB,QAAQ,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC;CAClD;AACD,UAAU,UAAU,CAAC,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;CAChG;AACD,KAAK,KAAK,CAAC,SAAS,IAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAAC;AAkBxF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,UACnC,gBAAgB,eACX,WAAW,CAAC,MAAM,CAAC,SACzB,KAAK,CAAC,SAAS,CAAC,KACtB,UAAU,CAAC,SAAS,CAqBtB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,OAAO,EAAE,CAAC,UAC9C,gBAAgB,eACX,WAAW,CAAC,MAAM,CAAC,UACxB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,UAChB,WAAW,CAAC,CAAC,CAAC,aACX,UAAU,CAAC,CAAC,CAAC,OACnB;IAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAAA;CAAE,KAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CA8BjC,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The reference leaf's privacy arm** (`PUB-009`), shared by `./subtype`
|
|
3
|
+
* and `./edges` so a reference row is read one way: as a list filter
|
|
4
|
+
* ({@link referenceListFilter}, behind `referencePrivacyFilter`, the subtype
|
|
5
|
+
* `list` and `./edges`' `endpointFilter`) and as a row predicate
|
|
6
|
+
* ({@link canViewReferenceRow}, behind `defineSubtype`'s `visibleRow` and
|
|
7
|
+
* `./edges`' `canViewEndpoint`). Before pulpit#92 each module spelled the
|
|
8
|
+
* predicate for itself.
|
|
9
|
+
*
|
|
10
|
+
* The row's facts are read by `ownedFacts`, so an owner-like value that is
|
|
11
|
+
* not a string reads as no owner and never reaches the follower seam
|
|
12
|
+
* (pulpit#85, pulpit#100).
|
|
13
|
+
*
|
|
14
|
+
* Internal: not a package entry point (absent from `package.json`
|
|
15
|
+
* `exports`) and excluded from the generated API docs (`docgen.json`).
|
|
16
|
+
* Typed structurally, so it never imports `./subtype`.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
import * as Effect from "effect/Effect";
|
|
21
|
+
import * as Match from "effect/Match";
|
|
22
|
+
import * as Option from "effect/Option";
|
|
23
|
+
import { always, and, eq, isIn, or } from "../filter.js";
|
|
24
|
+
import { ownedFacts } from "./owned-columns.js";
|
|
25
|
+
/** The reference axis's one column; a view without it rides no privacy axis. */
|
|
26
|
+
const VISIBILITY = "visibility";
|
|
27
|
+
/**
|
|
28
|
+
* The column whose followers may read a `followers` row, when the leaf
|
|
29
|
+
* declares one and the view carries it. A declared column the view lacks
|
|
30
|
+
* reads as none: the followers arm then matches nothing (fail closed).
|
|
31
|
+
*/
|
|
32
|
+
const followersColumn = (access, viewColumns) => Option.filter(Option.fromUndefinedOr(access.followersOfColumn), (column) => viewColumns.has(column));
|
|
33
|
+
/**
|
|
34
|
+
* The reference privacy arm as a list filter: `always` over a view without a
|
|
35
|
+
* `visibility` column or for the view-private rung (`scope.kind === "all"`);
|
|
36
|
+
* otherwise `visibility = 'public'`, and for a signed-in viewer also
|
|
37
|
+
* `visibility = 'followers' AND (<followersOfColumn> = <viewer> OR
|
|
38
|
+
* <followersOfColumn> IN (<following>))` when the leaf declares that column
|
|
39
|
+
* and the view carries it (pulpit#92): the account the column names and its
|
|
40
|
+
* followers read its `followers` rows. Its `private` rows stay the
|
|
41
|
+
* view-private rung's alone. Never throws.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export const referenceListFilter = (access, viewColumns, scope) => {
|
|
46
|
+
if (!viewColumns.has(VISIBILITY))
|
|
47
|
+
return always;
|
|
48
|
+
const column = followersColumn(access, viewColumns);
|
|
49
|
+
return Match.value(scope).pipe(Match.discriminatorsExhaustive("kind")({
|
|
50
|
+
all: () => always,
|
|
51
|
+
public: () => eq(VISIBILITY, "public"),
|
|
52
|
+
member: (member) => Option.match(column, {
|
|
53
|
+
onNone: () => eq(VISIBILITY, "public"),
|
|
54
|
+
onSome: (followed) => or(eq(VISIBILITY, "public"), and(eq(VISIBILITY, "followers"), or(eq(followed, member.userId), isIn(followed, member.following)))),
|
|
55
|
+
}),
|
|
56
|
+
}));
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The reference privacy arm as a row predicate, the same facts as
|
|
60
|
+
* {@link referenceListFilter}: a view without a `visibility` column shows
|
|
61
|
+
* the row; otherwise it shows when its visibility is exactly `public`, to
|
|
62
|
+
* the view-private rung, or, when it is exactly `followers` and the leaf
|
|
63
|
+
* declares a followers column the view carries, to the signed-in account
|
|
64
|
+
* that column names and to its followers, as the follower seam answers
|
|
65
|
+
* (pulpit#92). That account's `private` rows stay the view-private rung's
|
|
66
|
+
* alone. A column value that is not a string names no account, so it never
|
|
67
|
+
* matches the viewer and the seam is never asked about it (pulpit#85,
|
|
68
|
+
* pulpit#100). Anything else is hidden, which the
|
|
69
|
+
* caller answers as `NodeNotFound` (`SEC-AUTH-ENUM-001`). The soft-delete
|
|
70
|
+
* axis is the caller's. Never fails.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export const canViewReferenceRow = (access, viewColumns, viewer, policy, followers, row) => {
|
|
75
|
+
if (!viewColumns.has(VISIBILITY))
|
|
76
|
+
return Effect.succeed(true);
|
|
77
|
+
const visibility = row[VISIBILITY];
|
|
78
|
+
if (visibility === "public")
|
|
79
|
+
return Effect.succeed(true);
|
|
80
|
+
if (Option.isNone(viewer))
|
|
81
|
+
return Effect.succeed(false);
|
|
82
|
+
const account = viewer.value;
|
|
83
|
+
if (policy.canViewPrivate(account))
|
|
84
|
+
return Effect.succeed(true);
|
|
85
|
+
if (visibility !== "followers")
|
|
86
|
+
return Effect.succeed(false);
|
|
87
|
+
return Option.match(followersColumn(access, viewColumns), {
|
|
88
|
+
onNone: () => Effect.succeed(false),
|
|
89
|
+
onSome: (column) => Option.match(ownedFacts({
|
|
90
|
+
ownerColumn: column,
|
|
91
|
+
visibilityColumn: VISIBILITY,
|
|
92
|
+
draftColumn: Option.none(),
|
|
93
|
+
moderationColumn: Option.none(),
|
|
94
|
+
}, row).ownerId, {
|
|
95
|
+
onNone: () => Effect.succeed(false),
|
|
96
|
+
onSome: (ownerId) => ownerId === account.id
|
|
97
|
+
? Effect.succeed(true)
|
|
98
|
+
: followers.isFollower(account.id, ownerId),
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=reference-columns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-columns.js","sourceRoot":"","sources":["../../src/internal/reference-columns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,392 @@
|
|
|
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 Effect from "effect/Effect";
|
|
50
|
+
import * as Option from "effect/Option";
|
|
51
|
+
import * as Result from "effect/Result";
|
|
52
|
+
import * as Schema from "effect/Schema";
|
|
53
|
+
import type { BoundBudget } from "./budgets.ts";
|
|
54
|
+
import { type ListFilter } from "./filter.ts";
|
|
55
|
+
import { ModerationState } from "./publication.ts";
|
|
56
|
+
import { Forbidden, Unauthorized, WriteRejected, type AccessPolicy, type BeforeWriteEvent, type BeforeWriteHook, type Page, type SubtypeOps, type Viewer } from "./subtype.ts";
|
|
57
|
+
import type { ChainPatch, ChainWrite, NodeNotFound, Precondition, StaleVersion, VersionConflict } from "./version-chain.ts";
|
|
58
|
+
/**
|
|
59
|
+
* Whether a row may move from `from` (none for a row being created) to
|
|
60
|
+
* `to`: a new row only to `pending`; an existing one to any other state
|
|
61
|
+
* (decision, re-moderation, re-review). Staying put is no transition.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { strictEqual } from "node:assert";
|
|
66
|
+
* import * as Option from "effect/Option";
|
|
67
|
+
* import { isModerationTransition } from "@thomasfosterau/pulpit/moderation";
|
|
68
|
+
*
|
|
69
|
+
* strictEqual(isModerationTransition(Option.none(), "pending"), true);
|
|
70
|
+
* strictEqual(isModerationTransition(Option.none(), "approved"), false);
|
|
71
|
+
* strictEqual(isModerationTransition(Option.some("pending"), "approved"), true);
|
|
72
|
+
* strictEqual(isModerationTransition(Option.some("approved"), "approved"), false);
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export declare const isModerationTransition: (from: Option.Option<ModerationState>, to: ModerationState) => boolean;
|
|
78
|
+
/**
|
|
79
|
+
* A leaf's moderation columns and its re-review triggers.
|
|
80
|
+
*
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export interface ModerationDeclaration {
|
|
84
|
+
/** The state column ({@link ModerationState}); conventionally `moderationState`. */
|
|
85
|
+
readonly column: string;
|
|
86
|
+
/** The column a decision stamps with its instant, when the leaf has one. */
|
|
87
|
+
readonly moderatedAtColumn: Option.Option<string>;
|
|
88
|
+
/** The column a decision stamps with the deciding account's id, when the leaf has one. */
|
|
89
|
+
readonly moderatedByColumn: Option.Option<string>;
|
|
90
|
+
/**
|
|
91
|
+
* The attributes whose change sends an `approved` row back to `pending`
|
|
92
|
+
* (a comment's `body`, an image's file). Empty, an approval survives
|
|
93
|
+
* every edit.
|
|
94
|
+
*/
|
|
95
|
+
readonly reReviewOn: ReadonlyArray<string>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The declaration with the conventional column names.
|
|
99
|
+
*
|
|
100
|
+
* @since 0.1.0
|
|
101
|
+
*/
|
|
102
|
+
export declare const moderatedColumns: (reReviewOn: ReadonlyArray<string>) => ModerationDeclaration;
|
|
103
|
+
/**
|
|
104
|
+
* The moderation rules applied to one write: the patch the write should
|
|
105
|
+
* apply, or the reason it is refused. Pure, so every case is testable
|
|
106
|
+
* without a chain.
|
|
107
|
+
*
|
|
108
|
+
* - A **create** stores `pending`; a patch asking for another state is refused.
|
|
109
|
+
* - A **delete** is untouched (withdrawal needs no decision).
|
|
110
|
+
* - A **`restoreVersion`** by anyone but the moderate rung carries the
|
|
111
|
+
* prior row's moderation columns over the restored version's, so an old
|
|
112
|
+
* approval never comes back with old content.
|
|
113
|
+
* - A patch that **changes the state** is a decision: refused below the
|
|
114
|
+
* moderate rung or outside {@link isModerationTransition}, and otherwise
|
|
115
|
+
* stamped (`moderatedAtColumn`, `moderatedByColumn`). A patch that sets
|
|
116
|
+
* the state it already has changes nothing.
|
|
117
|
+
* - Otherwise, an edit that changes a `reReviewOn` attribute of an
|
|
118
|
+
* `approved` row sends it back to `pending`.
|
|
119
|
+
*
|
|
120
|
+
* @since 0.1.0
|
|
121
|
+
*/
|
|
122
|
+
export declare const moderatePatch: (event: BeforeWriteEvent, declaration: ModerationDeclaration, policy: AccessPolicy, now: Date) => Result.Result<ChainPatch, string>;
|
|
123
|
+
/**
|
|
124
|
+
* The moderation rules ({@link moderatePatch}) as a before hook for the
|
|
125
|
+
* leaf's `LifecycleHooks`: every write applies them, and a refusal
|
|
126
|
+
* fails the write with `WriteRejected`, appending nothing. Compose it first,
|
|
127
|
+
* so later hooks see the state it settled.
|
|
128
|
+
*
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
export declare const moderationHook: <E = never, R = never>(declaration: ModerationDeclaration, policy: AccessPolicy) => BeforeWriteHook<E, R>;
|
|
132
|
+
/**
|
|
133
|
+
* The operations {@link defineModeration} produces.
|
|
134
|
+
*
|
|
135
|
+
* @since 0.1.0
|
|
136
|
+
*/
|
|
137
|
+
export interface ModerationOps<E, R> {
|
|
138
|
+
/**
|
|
139
|
+
* Record a decision: append the state through the target's own `update`,
|
|
140
|
+
* which runs the {@link moderationHook} (stamping it). Fails
|
|
141
|
+
* `Unauthorized` when anonymous and `Forbidden` below the moderate rung,
|
|
142
|
+
* before anything is read, then as the target's `update` does
|
|
143
|
+
* (`NodeNotFound` for a missing or tombstoned row, `StaleVersion`, and
|
|
144
|
+
* `WriteRejected` from its hooks).
|
|
145
|
+
*/
|
|
146
|
+
readonly moderate: (id: string, decision: ModerationState, precondition?: Precondition) => Effect.Effect<ChainWrite, Unauthorized | Forbidden | NodeNotFound | StaleVersion | VersionConflict | WriteRejected | E, R>;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Bind a leaf's moderation decision. The leaf's hooks must include
|
|
150
|
+
* {@link moderationHook} with the same declaration: it is what admits the
|
|
151
|
+
* decision and stamps it.
|
|
152
|
+
*
|
|
153
|
+
* @since 0.1.0
|
|
154
|
+
*/
|
|
155
|
+
export declare const defineModeration: <Resource, E, R>(definition: {
|
|
156
|
+
readonly target: SubtypeOps<Resource, E, R>;
|
|
157
|
+
readonly declaration: ModerationDeclaration;
|
|
158
|
+
readonly viewer: Effect.Effect<Viewer, never, R>;
|
|
159
|
+
readonly policy: AccessPolicy;
|
|
160
|
+
}) => ModerationOps<E, R>;
|
|
161
|
+
/**
|
|
162
|
+
* The review queue's leaf half as a list filter: the `pending` rows, for
|
|
163
|
+
* the moderate rung only (`never` for anyone else). Pass it as a list
|
|
164
|
+
* request's `where`; the leaf's own read arms still apply.
|
|
165
|
+
*
|
|
166
|
+
* @since 0.1.0
|
|
167
|
+
*/
|
|
168
|
+
export declare const reviewQueueFilter: (viewer: Viewer, policy: AccessPolicy, column?: string) => ListFilter<never>;
|
|
169
|
+
/**
|
|
170
|
+
* A classifier's report, shaped like effect-ai-ext's `ModerationModel`
|
|
171
|
+
* output: its own flag, and a score in `[0, 1]` per category it assessed.
|
|
172
|
+
* A category it didn't assess is absent, never `0`.
|
|
173
|
+
*
|
|
174
|
+
* @since 0.1.0
|
|
175
|
+
*/
|
|
176
|
+
export interface ModerationScores {
|
|
177
|
+
readonly flagged: boolean;
|
|
178
|
+
readonly categoryScores: {
|
|
179
|
+
readonly [category: string]: number;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The band a flagged row lands in, from its worst breach.
|
|
184
|
+
*
|
|
185
|
+
* @since 0.1.0
|
|
186
|
+
*/
|
|
187
|
+
export declare const ModerationPriority: Schema.Literals<readonly ["low", "medium", "high", "urgent"]>;
|
|
188
|
+
/**
|
|
189
|
+
* The {@link ModerationPriority} literal union.
|
|
190
|
+
*
|
|
191
|
+
* @since 0.1.0
|
|
192
|
+
*/
|
|
193
|
+
export type ModerationPriority = typeof ModerationPriority.Type;
|
|
194
|
+
/**
|
|
195
|
+
* A graded report.
|
|
196
|
+
*
|
|
197
|
+
* @since 0.1.0
|
|
198
|
+
*/
|
|
199
|
+
export interface ModerationGrade {
|
|
200
|
+
/** The classifier's own flag, or a threshold breach. */
|
|
201
|
+
readonly isFlagged: boolean;
|
|
202
|
+
/** The categories that cleared their threshold, sorted. */
|
|
203
|
+
readonly exceeded: ReadonlyArray<string>;
|
|
204
|
+
/** The highest score across every category, or `0` when none was scored. */
|
|
205
|
+
readonly topScore: number;
|
|
206
|
+
readonly priority: ModerationPriority;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The bar a category with no configured threshold is graded against;
|
|
210
|
+
* strictly above it flags. A starting point to tune, not a measured value.
|
|
211
|
+
*
|
|
212
|
+
* @since 0.1.0
|
|
213
|
+
*/
|
|
214
|
+
export declare const DEFAULT_MODERATION_THRESHOLD = 0.5;
|
|
215
|
+
/**
|
|
216
|
+
* Grade a classifier's scores against the leaf's thresholds (a category
|
|
217
|
+
* missing from `thresholds` grades against `defaultThreshold`). A category
|
|
218
|
+
* breaches when its score is strictly above its bar; an unassessed one is
|
|
219
|
+
* absent and can't breach. The classifier's own flag is never overridden
|
|
220
|
+
* downward, only added to (raise a category's bar to `1` to ignore it). The
|
|
221
|
+
* band is the worst breaching score's, or the top score's for a flag with
|
|
222
|
+
* no breach. Pure.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { deepStrictEqual } from "node:assert";
|
|
227
|
+
* import { gradeScores } from "@thomasfosterau/pulpit/moderation";
|
|
228
|
+
*
|
|
229
|
+
* deepStrictEqual(
|
|
230
|
+
* gradeScores({ flagged: false, categoryScores: { hate: 0.8, spam: 0.3 } }, { spam: 0.2 }),
|
|
231
|
+
* { isFlagged: true, exceeded: ["hate", "spam"], topScore: 0.8, priority: "high" },
|
|
232
|
+
* );
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* @since 0.1.0
|
|
236
|
+
*/
|
|
237
|
+
export declare const gradeScores: (scores: ModerationScores, thresholds: {
|
|
238
|
+
readonly [category: string]: number;
|
|
239
|
+
}, defaultThreshold?: number) => ModerationGrade;
|
|
240
|
+
/**
|
|
241
|
+
* A report's status: `open` until the moderate rung resolves it (acted on)
|
|
242
|
+
* or dismisses it.
|
|
243
|
+
*
|
|
244
|
+
* @since 0.1.0
|
|
245
|
+
*/
|
|
246
|
+
export declare const ReportStatus: Schema.Literals<readonly ["open", "resolved", "dismissed"]>;
|
|
247
|
+
/**
|
|
248
|
+
* The {@link ReportStatus} literal union.
|
|
249
|
+
*
|
|
250
|
+
* @since 0.1.0
|
|
251
|
+
*/
|
|
252
|
+
export type ReportStatus = typeof ReportStatus.Type;
|
|
253
|
+
/**
|
|
254
|
+
* One member report, filed against a node.
|
|
255
|
+
*
|
|
256
|
+
* @since 0.1.0
|
|
257
|
+
*/
|
|
258
|
+
export interface ReportRow {
|
|
259
|
+
readonly id: string;
|
|
260
|
+
readonly targetId: string;
|
|
261
|
+
readonly targetType: string;
|
|
262
|
+
readonly reporterId: string;
|
|
263
|
+
/** The application's reason code (`spam`, `harassment`, …). */
|
|
264
|
+
readonly reason: string;
|
|
265
|
+
readonly details: Option.Option<string>;
|
|
266
|
+
readonly status: ReportStatus;
|
|
267
|
+
readonly filedAt: Date;
|
|
268
|
+
readonly resolvedAt: Option.Option<Date>;
|
|
269
|
+
readonly resolvedBy: Option.Option<string>;
|
|
270
|
+
readonly resolutionNote: Option.Option<string>;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The write that closes an open report.
|
|
274
|
+
*
|
|
275
|
+
* @since 0.1.0
|
|
276
|
+
*/
|
|
277
|
+
export interface ReportResolution {
|
|
278
|
+
readonly status: Exclude<ReportStatus, "open">;
|
|
279
|
+
readonly resolvedAt: Date;
|
|
280
|
+
readonly resolvedBy: string;
|
|
281
|
+
readonly resolutionNote: Option.Option<string>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The report table an application supplies. It applies no policy, never
|
|
285
|
+
* opens a transaction, and fails only with its `E` (README → Seams).
|
|
286
|
+
*
|
|
287
|
+
* @since 0.1.0
|
|
288
|
+
*/
|
|
289
|
+
export interface ReportStore<E, R> {
|
|
290
|
+
readonly mintId: Effect.Effect<string, E, R>;
|
|
291
|
+
readonly insert: (row: ReportRow) => Effect.Effect<void, E, R>;
|
|
292
|
+
readonly get: (id: string) => Effect.Effect<Option.Option<ReportRow>, E, R>;
|
|
293
|
+
/** One row, `FOR UPDATE`, so two resolutions serialise. */
|
|
294
|
+
readonly lock: (id: string) => Effect.Effect<Option.Option<ReportRow>, E, R>;
|
|
295
|
+
readonly close: (id: string, resolution: ReportResolution) => Effect.Effect<void, E, R>;
|
|
296
|
+
/**
|
|
297
|
+
* The page under `where`, newest first (`filed_at DESC, id DESC`), with
|
|
298
|
+
* the unpaged total. `where` reads `reporterId`, `targetId`, `targetType`
|
|
299
|
+
* and `status`.
|
|
300
|
+
*/
|
|
301
|
+
readonly list: (query: {
|
|
302
|
+
readonly where: ListFilter<never>;
|
|
303
|
+
readonly limit: number;
|
|
304
|
+
readonly offset: number;
|
|
305
|
+
}) => Effect.Effect<Page<ReportRow>, E, R>;
|
|
306
|
+
}
|
|
307
|
+
declare const ReportNotFound_base: Schema.Class<ReportNotFound, Schema.TaggedStruct<"ReportNotFound", {
|
|
308
|
+
readonly id: Schema.String;
|
|
309
|
+
}>, import("effect/Cause").YieldableError & {
|
|
310
|
+
readonly brand: unique symbol;
|
|
311
|
+
}>;
|
|
312
|
+
/**
|
|
313
|
+
* No report at that id the viewer may read: the wire's 404.
|
|
314
|
+
*
|
|
315
|
+
* @since 0.1.0
|
|
316
|
+
*/
|
|
317
|
+
export declare class ReportNotFound extends ReportNotFound_base {
|
|
318
|
+
}
|
|
319
|
+
declare const ReportClosed_base: Schema.Class<ReportClosed, Schema.TaggedStruct<"ReportClosed", {
|
|
320
|
+
readonly id: Schema.String;
|
|
321
|
+
readonly status: Schema.Literals<readonly ["open", "resolved", "dismissed"]>;
|
|
322
|
+
}>, import("effect/Cause").YieldableError & {
|
|
323
|
+
readonly brand: unique symbol;
|
|
324
|
+
}>;
|
|
325
|
+
/**
|
|
326
|
+
* The report is already resolved or dismissed: the wire's 409.
|
|
327
|
+
*
|
|
328
|
+
* @since 0.1.0
|
|
329
|
+
*/
|
|
330
|
+
export declare class ReportClosed extends ReportClosed_base {
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The operations {@link defineReports} produces.
|
|
334
|
+
*
|
|
335
|
+
* @since 0.1.0
|
|
336
|
+
*/
|
|
337
|
+
export interface ReportOps<E, R> {
|
|
338
|
+
/**
|
|
339
|
+
* File a report against `targetId`. Fails `Unauthorized` when anonymous
|
|
340
|
+
* and `NodeNotFound` when the reporter can't `get` the target, never
|
|
341
|
+
* `Forbidden` (`SEC-AUTH-ENUM-001`), and `WriteRejected` (`RATE_LIMITED`)
|
|
342
|
+
* when the declared budget is spent. The answer is the reporter's own
|
|
343
|
+
* report: never whether others reported the same node.
|
|
344
|
+
*/
|
|
345
|
+
readonly file: (input: {
|
|
346
|
+
readonly targetId: string;
|
|
347
|
+
readonly reason: string;
|
|
348
|
+
readonly details: Option.Option<string>;
|
|
349
|
+
}) => Effect.Effect<{
|
|
350
|
+
readonly id: string;
|
|
351
|
+
}, Unauthorized | NodeNotFound | WriteRejected | E, R>;
|
|
352
|
+
/** One report, for its reporter or the moderate rung; `ReportNotFound` otherwise. */
|
|
353
|
+
readonly get: (id: string) => Effect.Effect<ReportRow, ReportNotFound | E, R>;
|
|
354
|
+
/**
|
|
355
|
+
* One page, newest first: every report for the moderate rung (the queue's
|
|
356
|
+
* report half, by `status: open`), the viewer's own for anyone else.
|
|
357
|
+
*/
|
|
358
|
+
readonly list: (request: {
|
|
359
|
+
readonly targetId: Option.Option<string>;
|
|
360
|
+
readonly status: Option.Option<ReportStatus>;
|
|
361
|
+
readonly limit: number;
|
|
362
|
+
readonly offset: number;
|
|
363
|
+
}) => Effect.Effect<Page<ReportRow>, E, R>;
|
|
364
|
+
/**
|
|
365
|
+
* Close an open report as `resolved` or `dismissed`. Fails `Unauthorized`
|
|
366
|
+
* / `Forbidden` below the moderate rung, before the report is read, then
|
|
367
|
+
* `ReportNotFound` and `ReportClosed`. Acting on the node is the target's
|
|
368
|
+
* own operation (`moderate`, a delete), called separately.
|
|
369
|
+
*/
|
|
370
|
+
readonly resolve: (id: string, outcome: Exclude<ReportStatus, "open">, note: Option.Option<string>) => Effect.Effect<void, Unauthorized | Forbidden | ReportNotFound | ReportClosed | E, R>;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Bind member reports against one leaf: filed by a signed-in account that
|
|
374
|
+
* can read the target, read by their reporter and the moderate rung, and
|
|
375
|
+
* resolved by the moderate rung.
|
|
376
|
+
*
|
|
377
|
+
* @since 0.1.0
|
|
378
|
+
*/
|
|
379
|
+
export declare const defineReports: <Resource, E, R>(definition: {
|
|
380
|
+
readonly target: SubtypeOps<Resource, E, R>;
|
|
381
|
+
readonly reports: ReportStore<E, R>;
|
|
382
|
+
readonly viewer: Effect.Effect<Viewer, never, R>;
|
|
383
|
+
readonly policy: AccessPolicy;
|
|
384
|
+
/**
|
|
385
|
+
* A write budget on `file` (pulpit#199, `./budgets`' `bindBudget`), spent
|
|
386
|
+
* as the `file` action once the reporter can read the target and before
|
|
387
|
+
* the report is stored. Omitted, reports are unthrottled.
|
|
388
|
+
*/
|
|
389
|
+
readonly budget?: BoundBudget<E, R> | undefined;
|
|
390
|
+
}) => ReportOps<E, R>;
|
|
391
|
+
export {};
|
|
392
|
+
//# sourceMappingURL=moderation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation.d.ts","sourceRoot":"","sources":["../src/moderation.ts"],"names":[],"mappings":"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"}
|