@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.30
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/README.md +39 -0
- package/package.json +13 -6
- package/src/apiAnnotations.d.ts +508 -0
- package/src/apiAnnotations.js +543 -0
- package/src/apiDocuments.d.ts +569 -0
- package/src/apiDocuments.js +551 -0
- package/src/apiLayers.d.ts +369 -0
- package/src/apiLayers.js +222 -0
- package/src/apiMessages.d.ts +293 -0
- package/src/apiMessages.js +265 -0
- package/src/apiPeople.d.ts +626 -0
- package/src/apiPeople.js +698 -0
- package/src/apiProject.d.ts +761 -0
- package/src/apiProject.js +774 -0
- package/src/apiServer.d.ts +455 -0
- package/src/apiServer.js +422 -0
- package/src/apiVocab.d.ts +456 -0
- package/src/apiVocab.js +425 -0
- package/src/batch.d.ts +54 -0
- package/src/batch.js +323 -0
- package/src/batchRef.d.ts +30 -0
- package/src/batchRef.js +128 -0
- package/src/client.d.ts +1321 -0
- package/src/client.js +743 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +85 -0
- package/src/constraints.js +112 -0
- package/src/created.d.ts +2 -0
- package/src/created.js +29 -0
- package/src/documentLock.d.ts +172 -0
- package/src/documentLock.js +384 -0
- package/src/entryForms.d.ts +63 -0
- package/src/entryForms.js +152 -0
- package/src/events.d.ts +60 -0
- package/src/events.js +271 -0
- package/src/families.d.ts +349 -0
- package/src/families.js +396 -0
- package/src/http.d.ts +233 -0
- package/src/http.js +809 -93
- package/src/ids.d.ts +26 -0
- package/src/ids.js +96 -0
- package/src/index.d.ts +101 -0
- package/src/index.js +183 -2105
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +124 -0
- package/src/operations.d.ts +36 -0
- package/src/operations.js +164 -0
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +20 -53
- package/src/provenance.d.ts +243 -0
- package/src/provenance.js +458 -36
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +31 -0
- package/src/requester.d.ts +60 -0
- package/src/requester.js +74 -0
- package/src/roles.d.ts +104 -0
- package/src/roles.js +64 -2
- package/src/serviceSchema.d.ts +337 -0
- package/src/serviceSchema.js +177 -60
- package/src/services.d.ts +319 -0
- package/src/services.js +742 -58
- package/src/sse.d.ts +30 -0
- package/src/sse.js +80 -3
- package/src/testing.d.ts +128 -0
- package/src/testing.js +379 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +234 -0
- package/src/transforms.d.ts +25 -0
- package/src/transforms.js +11 -1
- package/src/types.d.ts +867 -0
- package/src/types.js +663 -0
- package/index.d.ts +0 -532
package/src/provenance.js
CHANGED
|
@@ -5,28 +5,53 @@
|
|
|
5
5
|
* Flat scalar keys — the query engine matches flat metadata well, nested
|
|
6
6
|
* objects poorly.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Two axes. ORIGIN is the `prov` key: absent means a trusted person (a
|
|
9
|
+
* verifier) made it; 'inferred' means an algorithm or service did;
|
|
10
|
+
* 'contributed' means a person whose work the project reviews (a
|
|
11
|
+
* contributor) did. TRUST is `provConfirmed`: true once a verifier vouched
|
|
12
|
+
* for the value. Together they give four states:
|
|
10
13
|
*
|
|
11
|
-
* human — no prov keys; a
|
|
14
|
+
* human — no prov keys; a verifier made it.
|
|
12
15
|
* machine — { prov: 'inferred', provSource: '<producer>' };
|
|
13
|
-
* an algorithm/service made it,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* an algorithm/service made it, nobody has vouched.
|
|
17
|
+
* contributed — { prov: 'contributed', provSource: 'user:<id>' };
|
|
18
|
+
* a contributor made it, no verifier has vouched.
|
|
19
|
+
* verified — either origin + { provConfirmed: true }; a verifier
|
|
20
|
+
* confirmed (or edited) it. prov/provSource stay so
|
|
21
|
+
* the origin remains traceable.
|
|
17
22
|
*
|
|
18
23
|
* The write contract every machine writer must follow:
|
|
19
24
|
* 1. Machine writers may freely replace MACHINE (unverified) material.
|
|
20
|
-
* 2. Machine writers must never modify or delete human or
|
|
21
|
-
* material unless explicitly told to overwrite (an explicit,
|
|
22
|
-
* user-facing opt-in — for services, a declared boolean
|
|
23
|
-
* parameter).
|
|
24
|
-
* 3.
|
|
25
|
-
* stamps { provConfirmed: true } (see verifyOnEdit).
|
|
25
|
+
* 2. Machine writers must never modify or delete human, contributed or
|
|
26
|
+
* verified material unless explicitly told to overwrite (an explicit,
|
|
27
|
+
* per-run, user-facing opt-in — for services, a declared boolean
|
|
28
|
+
* `overwrite` parameter). A contributor's work is a person's work.
|
|
29
|
+
* 3. A verifier's edit of machine or contributed material verifies it:
|
|
30
|
+
* the edit also stamps { provConfirmed: true } (see verifyOnEdit).
|
|
31
|
+
* A contributor's edit of anything marks it contributed: the edit
|
|
32
|
+
* merges stampContributed(userId) and drops any earlier confirmation
|
|
33
|
+
* (see contributeOnEdit). Who is a contributor is the PROJECT's call,
|
|
34
|
+
* recorded once for every app (see "Review" below); a service running
|
|
35
|
+
* as a contributor should stamp likewise.
|
|
36
|
+
*
|
|
37
|
+
* REVIEW: which people are contributors. A project records it under the
|
|
38
|
+
* reserved `plaid` config namespace, so every app that writes on a person's
|
|
39
|
+
* behalf reads the same answer:
|
|
40
|
+
*
|
|
41
|
+
* config.plaid.review = { users: [userId, ...], roles: ['writer', ...] }
|
|
42
|
+
*
|
|
43
|
+
* `users` names people whose work is reviewed whatever their role; `roles`
|
|
44
|
+
* names whole project roles ('reader' | 'writer' | 'maintainer'; an admin
|
|
45
|
+
* without an explicit role counts as a maintainer). Either may be absent.
|
|
46
|
+
* Nothing here grants or denies access: the ACL lists stay the permission
|
|
47
|
+
* model, and this only says whose work needs a verifier's look. Apps expose
|
|
48
|
+
* it as a per-member mark on their access screens (readReview, isReviewed,
|
|
49
|
+
* withReviewedUser) and derive the writer's policy from it (writerPolicy).
|
|
26
50
|
*
|
|
27
51
|
* Producer naming: 'service:<serviceId>' for services (use serviceSource),
|
|
28
|
-
* 'rule:<name>' for built-in rule algorithms,
|
|
29
|
-
* 'gloss:doc-frequency'
|
|
52
|
+
* 'rule:<name>' for built-in rule algorithms, 'user:<userId>' for a
|
|
53
|
+
* contributor (use userSource), app-specific ids like 'gloss:doc-frequency'
|
|
54
|
+
* or 'flex-import' otherwise.
|
|
30
55
|
*
|
|
31
56
|
* PREDICTION EXTRAS. A producer may also record how confident it was and
|
|
32
57
|
* what else it considered, in two reserved slots split along the
|
|
@@ -35,14 +60,43 @@
|
|
|
35
60
|
* the value it chose. Flat scalars are what the query engine filters
|
|
36
61
|
* and orders on, so "review the least-confident machine output first"
|
|
37
62
|
* is an ordinary query. Omit it unless you can honestly produce a
|
|
38
|
-
* probability (a raw logprob is NOT one — put it in provDetail)
|
|
63
|
+
* probability (a raw logprob is NOT one — put it in provDetail), and
|
|
64
|
+
* calibrate it on held-out data, so that values stated at 70% are
|
|
65
|
+
* right about 70% of the time. Apps mark a value below the project's
|
|
66
|
+
* threshold (readConfidenceThreshold), one number for every producer.
|
|
39
67
|
* - provDetail: ONE open map for everything else — top-k alternatives or
|
|
40
68
|
* distributions, model name/version, raw scores. Deliberately nested
|
|
41
69
|
* (not queryable); keep it small (top-k, not whole-vocabulary dumps).
|
|
42
|
-
*
|
|
43
|
-
* human edits (history is valuable)
|
|
44
|
-
*
|
|
70
|
+
* provDetail describes the machine's ORIGINAL prediction and is kept after
|
|
71
|
+
* human edits (history is valuable). provProb is the producer's probability
|
|
72
|
+
* for the value the entity holds, however a person entered it: a value the
|
|
73
|
+
* producer weighed (one in provDetail.valueProbs, or formProbs for a
|
|
74
|
+
* morpheme's form) carries that probability, and a value it never weighed
|
|
75
|
+
* carries none (writerPolicy's valueEditStamp is the one implementation,
|
|
76
|
+
* and it reads the distribution itself). A consumer still must not present
|
|
77
|
+
* provProb as the machine's confidence once the entity is verified
|
|
45
78
|
* (provConfirmed is exactly the flag to check).
|
|
79
|
+
*
|
|
80
|
+
* Recommended provDetail keys, so consumers (review dashboards, ranked
|
|
81
|
+
* alternatives) can read any producer's output:
|
|
82
|
+
* - value (spans) / form (morpheme tokens): the value the producer
|
|
83
|
+
* wrote, mirroring the entity's own field. The entity may be edited
|
|
84
|
+
* later; this copy is what makes "accepted as-is" vs "corrected"
|
|
85
|
+
* answerable once the entity is verified.
|
|
86
|
+
* - valueProbs / formProbs: a top-k {label: probability} map over the
|
|
87
|
+
* alternatives the producer considered, the chosen label included
|
|
88
|
+
* (provProb, when given, is that label's entry duplicated flat).
|
|
89
|
+
* - valueLinks: on a gloss, a list of {value, item, prob}, one per
|
|
90
|
+
* alternative that stands for a lexicon entry: its label, the vocab
|
|
91
|
+
* item id and its probability (two senses may share a label, so this
|
|
92
|
+
* is a list, not a map). igt offers each alternative together with
|
|
93
|
+
* its entry as one choice, under Alt+Down on the gloss and in the
|
|
94
|
+
* lexicon popover.
|
|
95
|
+
* - model: what made the prediction (a model name, or a tool and its
|
|
96
|
+
* release such as 'stanza==1.11.0'), and version: which version of the
|
|
97
|
+
* writer ran ('<release>+<8 hex>', see the manual, "Provenance"). Every
|
|
98
|
+
* machine writer records both, one of rules alone only version.
|
|
99
|
+
* - anything else (language, adapter, raw scores).
|
|
46
100
|
*/
|
|
47
101
|
|
|
48
102
|
/** The flat metadata keys of the provenance convention. */
|
|
@@ -57,31 +111,65 @@ export const PROV = Object.freeze({
|
|
|
57
111
|
probKey: 'provProb',
|
|
58
112
|
/** Open map of producer extras (alternatives, model version, raw scores). */
|
|
59
113
|
detailKey: 'provDetail',
|
|
60
|
-
/**
|
|
114
|
+
/** `prov` for machine-made material. Any value but CONTRIBUTED reads as machine. */
|
|
61
115
|
INFERRED: 'inferred',
|
|
116
|
+
/** `prov` for a contributor's material. */
|
|
117
|
+
CONTRIBUTED: 'contributed',
|
|
62
118
|
});
|
|
63
119
|
|
|
64
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Every provenance key, for a caller that has to name them as a set rather
|
|
122
|
+
* than read one: what a token layer declares under
|
|
123
|
+
* `config.plaid.preserveOnSplit` so a token born of a split inherits the
|
|
124
|
+
* origin of the token it came from, and what a reshape carries by hand.
|
|
125
|
+
*
|
|
126
|
+
* Provenance is the one metadata family Plaid itself owns. The user cannot
|
|
127
|
+
* maintain it, and in a shared project usually cannot even see it, since it
|
|
128
|
+
* may belong to an app they are not using. Everything else on an entity is
|
|
129
|
+
* the user's own content and no structural operation should touch it.
|
|
130
|
+
*/
|
|
131
|
+
export const PROVENANCE_KEYS = Object.freeze([
|
|
132
|
+
PROV.key,
|
|
133
|
+
PROV.sourceKey,
|
|
134
|
+
PROV.confirmedKey,
|
|
135
|
+
PROV.probKey,
|
|
136
|
+
PROV.detailKey,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/** The four provenance states returned by provState. */
|
|
65
140
|
export const PROV_STATES = Object.freeze({
|
|
66
141
|
HUMAN: 'human',
|
|
67
142
|
MACHINE: 'machine',
|
|
143
|
+
CONTRIBUTED: 'contributed',
|
|
68
144
|
VERIFIED: 'verified',
|
|
69
145
|
});
|
|
70
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Whether `p` is a probability, a finite number in [0, 1]: the only thing
|
|
149
|
+
* written as provProb. A stamp given anything else writes no provProb.
|
|
150
|
+
* @param {*} p
|
|
151
|
+
* @returns {boolean}
|
|
152
|
+
*/
|
|
153
|
+
const isProbability = (p) => typeof p === 'number' && p >= 0 && p <= 1;
|
|
154
|
+
|
|
71
155
|
/**
|
|
72
156
|
* The metadata fragment a machine writer merges into everything it creates.
|
|
73
157
|
* @param {string} source - producer id, e.g. serviceSource(serviceId)
|
|
74
158
|
* @param {Object} [extras] - optional prediction extras:
|
|
75
159
|
* @param {number} [extras.prob] - probability in [0,1] for the chosen value
|
|
76
|
-
* (omit unless it honestly is one — raw scores go in detail)
|
|
160
|
+
* (omit unless it honestly is one — raw scores go in detail). Anything
|
|
161
|
+
* else is left out.
|
|
77
162
|
* @param {Object} [extras.detail] - open map of producer extras (top-k
|
|
78
163
|
* alternatives, model version, raw scores; keep it small)
|
|
79
164
|
*/
|
|
80
165
|
export const stampInferred = (source, { prob, detail } = {}) => ({
|
|
81
166
|
[PROV.key]: PROV.INFERRED,
|
|
82
167
|
[PROV.sourceKey]: source,
|
|
83
|
-
...(prob
|
|
84
|
-
|
|
168
|
+
...(isProbability(prob) ? { [PROV.probKey]: prob } : {}),
|
|
169
|
+
// An empty detail is no detail, as in the Python client.
|
|
170
|
+
...(detail && !(typeof detail === 'object' && Object.keys(detail).length === 0)
|
|
171
|
+
? { [PROV.detailKey]: detail }
|
|
172
|
+
: {}),
|
|
85
173
|
});
|
|
86
174
|
|
|
87
175
|
/**
|
|
@@ -89,6 +177,8 @@ export const stampInferred = (source, { prob, detail } = {}) => ({
|
|
|
89
177
|
* import carrying upstream human approval, or a guess written only on
|
|
90
178
|
* explicit user confirmation). Takes the same optional prediction extras
|
|
91
179
|
* as stampInferred.
|
|
180
|
+
* @param {string} source
|
|
181
|
+
* @param {{prob?: number, detail?: Record<string, any>}} [extras]
|
|
92
182
|
*/
|
|
93
183
|
export const confirmedInferred = (source, extras = {}) => ({
|
|
94
184
|
...stampInferred(source, extras),
|
|
@@ -105,35 +195,367 @@ export const confirmedInferred = (source, extras = {}) => ({
|
|
|
105
195
|
*/
|
|
106
196
|
export const PROV_CONFIRMED = Object.freeze({ [PROV.confirmedKey]: true });
|
|
107
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The metadata fragment a contributor's work carries:
|
|
200
|
+
* { prov: 'contributed', provSource: 'user:<userId>' }.
|
|
201
|
+
* @param {string} userId
|
|
202
|
+
*/
|
|
203
|
+
export const stampContributed = (userId) => ({
|
|
204
|
+
[PROV.key]: PROV.CONTRIBUTED,
|
|
205
|
+
[PROV.sourceKey]: userSource(userId),
|
|
206
|
+
});
|
|
207
|
+
|
|
108
208
|
/**
|
|
109
209
|
* Classify an entity's metadata into one of PROV_STATES.
|
|
110
|
-
* @param {
|
|
111
|
-
* @returns {'human'|'machine'|'verified'}
|
|
210
|
+
* @param {object|null|undefined} metadata
|
|
211
|
+
* @returns {'human'|'machine'|'contributed'|'verified'}
|
|
112
212
|
*/
|
|
113
213
|
export const provState = (metadata) => {
|
|
114
214
|
if (!metadata || metadata[PROV.key] == null) return PROV_STATES.HUMAN;
|
|
115
|
-
|
|
215
|
+
if (metadata[PROV.confirmedKey]) return PROV_STATES.VERIFIED;
|
|
216
|
+
return metadata[PROV.key] === PROV.CONTRIBUTED ? PROV_STATES.CONTRIBUTED : PROV_STATES.MACHINE;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Where an entity came from, confirmed or not: null for a verifier's own
|
|
221
|
+
* work, PROV.INFERRED for a machine's, PROV.CONTRIBUTED for a contributor's.
|
|
222
|
+
* What a verified entity's tooltip needs, since provState folds both
|
|
223
|
+
* origins into 'verified'.
|
|
224
|
+
* @param {object|null|undefined} metadata
|
|
225
|
+
* @returns {null|'inferred'|'contributed'}
|
|
226
|
+
*/
|
|
227
|
+
export const provOrigin = (metadata) => {
|
|
228
|
+
if (!metadata || metadata[PROV.key] == null) return null;
|
|
229
|
+
return metadata[PROV.key] === PROV.CONTRIBUTED ? PROV.CONTRIBUTED : PROV.INFERRED;
|
|
116
230
|
};
|
|
117
231
|
|
|
118
232
|
/**
|
|
119
|
-
* Whether this entity is machine-made and not yet
|
|
120
|
-
*
|
|
121
|
-
*
|
|
233
|
+
* Whether this entity is machine-made and not yet verified — the material
|
|
234
|
+
* machine writers may replace. Complement of isProtected.
|
|
235
|
+
* @param {object|null|undefined} metadata
|
|
236
|
+
* @returns {boolean}
|
|
122
237
|
*/
|
|
123
238
|
export const isMachine = (metadata) => provState(metadata) === PROV_STATES.MACHINE;
|
|
124
239
|
|
|
125
240
|
/**
|
|
126
241
|
* Whether a machine writer must leave this entity alone (write-contract
|
|
127
|
-
* rule 2): true for human-made and
|
|
242
|
+
* rule 2): true for human-made, contributed and verified material.
|
|
243
|
+
* @param {object|null|undefined} metadata
|
|
244
|
+
* @returns {boolean}
|
|
128
245
|
*/
|
|
129
246
|
export const isProtected = (metadata) => !isMachine(metadata);
|
|
130
247
|
|
|
131
248
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
249
|
+
* Whether a verifier still has to look at this entity: machine-made or
|
|
250
|
+
* contributed, and not yet confirmed. What review UIs mark and sweep, and
|
|
251
|
+
* what a verifier's confirmation gesture acts on.
|
|
252
|
+
* @param {object|null|undefined} metadata
|
|
253
|
+
* @returns {boolean}
|
|
135
254
|
*/
|
|
136
|
-
export const
|
|
255
|
+
export const needsReview = (metadata) => {
|
|
256
|
+
const s = provState(metadata);
|
|
257
|
+
return s === PROV_STATES.MACHINE || s === PROV_STATES.CONTRIBUTED;
|
|
258
|
+
};
|
|
137
259
|
|
|
138
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* The metadata fragment a VERIFIER's edit of this entity should merge in
|
|
262
|
+
* (write-contract rule 3): PROV_CONFIRMED when the entity needs review,
|
|
263
|
+
* else null (nothing to do).
|
|
264
|
+
* @param {object|null|undefined} metadata
|
|
265
|
+
*/
|
|
266
|
+
export const verifyOnEdit = (metadata) => (needsReview(metadata) ? PROV_CONFIRMED : null);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The metadata fragment a CONTRIBUTOR's edit of this entity should merge in
|
|
270
|
+
* (write-contract rule 3): the contributed stamp, plus provConfirmed: null
|
|
271
|
+
* so an earlier confirmation is dropped (send it with metadataOps, which
|
|
272
|
+
* turns a null into a delete, or apply it locally with mergeMetadata). Prediction extras a machine
|
|
273
|
+
* recorded stay, as history.
|
|
274
|
+
* @param {object|null|undefined} metadata - unused today; the fragment is
|
|
275
|
+
* the same whatever the entity was, and the parameter keeps the shape of
|
|
276
|
+
* verifyOnEdit so callers can swap one for the other
|
|
277
|
+
* @param {string} userId
|
|
278
|
+
* @returns {{prov: "contributed", provSource: string, provConfirmed: null}}
|
|
279
|
+
*/
|
|
280
|
+
// eslint-disable-next-line no-unused-vars
|
|
281
|
+
export const contributeOnEdit = (metadata, userId) => ({
|
|
282
|
+
...stampContributed(userId),
|
|
283
|
+
[PROV.confirmedKey]: null,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Canonical provSource for a service: 'service:<serviceId>'.
|
|
288
|
+
* @param {string} serviceId
|
|
289
|
+
* @returns {string}
|
|
290
|
+
*/
|
|
139
291
|
export const serviceSource = (serviceId) => `service:${serviceId}`;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Canonical provSource for a contributor: 'user:<userId>'.
|
|
295
|
+
* @param {string} userId
|
|
296
|
+
* @returns {string}
|
|
297
|
+
*/
|
|
298
|
+
export const userSource = (userId) => `user:${userId}`;
|
|
299
|
+
|
|
300
|
+
// ---- review: whose work is reviewed (a project-config norm) ----
|
|
301
|
+
|
|
302
|
+
/** The config key, under the `plaid` namespace, holding the review lists. */
|
|
303
|
+
export const REVIEW_KEY = 'review';
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The project's review lists, normalized: { users: string[], roles: string[] }.
|
|
307
|
+
* @param {object|null} [config] - a project's `config`
|
|
308
|
+
* @returns {{users: string[], roles: string[]}}
|
|
309
|
+
*/
|
|
310
|
+
export const readReview = (config) => {
|
|
311
|
+
const raw = config?.plaid?.[REVIEW_KEY];
|
|
312
|
+
const list = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
|
|
313
|
+
return { users: list(raw?.users), roles: list(raw?.roles) };
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Whether the project reviews anybody's work: a person or a role is named in
|
|
318
|
+
* its review lists. Where it reviews nobody, nothing is contributed, and an
|
|
319
|
+
* editor's legend has no contributed mark to explain.
|
|
320
|
+
* @param {object|null} [config] - a project's `config`
|
|
321
|
+
* @returns {boolean}
|
|
322
|
+
*/
|
|
323
|
+
export const reviewsAnyone = (config) => {
|
|
324
|
+
const { users, roles } = readReview(config);
|
|
325
|
+
return users.length > 0 || roles.length > 0;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A person's role in a project from its ACL lists: 'maintainer' | 'writer' |
|
|
330
|
+
* 'reader' | null. An admin with no explicit entry is a maintainer, as the
|
|
331
|
+
* permission model treats them.
|
|
332
|
+
* @param {object} project - with `maintainers`, `writers`, `readers`
|
|
333
|
+
* @param {string} userId
|
|
334
|
+
* @param {Object} [opts]
|
|
335
|
+
* @param {boolean} [opts.isAdmin]
|
|
336
|
+
*/
|
|
337
|
+
export const projectRole = (project, userId, { isAdmin = false } = {}) => {
|
|
338
|
+
const inList = (l) => Array.isArray(l) && userId != null && l.includes(userId);
|
|
339
|
+
if (inList(project?.maintainers)) return 'maintainer';
|
|
340
|
+
if (inList(project?.writers)) return 'writer';
|
|
341
|
+
if (inList(project?.readers)) return 'reader';
|
|
342
|
+
return isAdmin ? 'maintainer' : null;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether this person's work is reviewed in this project: named in
|
|
347
|
+
* review.users, or holding a role named in review.roles.
|
|
348
|
+
* @param {object|null|undefined} project
|
|
349
|
+
* @param {string|null|undefined} userId
|
|
350
|
+
* @param {{isAdmin?: boolean}} [opts]
|
|
351
|
+
* @returns {boolean}
|
|
352
|
+
*/
|
|
353
|
+
export const isReviewed = (project, userId, { isAdmin = false } = {}) => {
|
|
354
|
+
if (!project || userId == null) return false;
|
|
355
|
+
const { users, roles } = readReview(project.config);
|
|
356
|
+
if (users.includes(userId)) return true;
|
|
357
|
+
const role = projectRole(project, userId, { isAdmin });
|
|
358
|
+
return role != null && roles.includes(role);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The review lists with one person added to or removed from `users`
|
|
363
|
+
* (`roles` untouched). Pure; write the result with
|
|
364
|
+
* projects.setConfig(id, 'plaid', REVIEW_KEY, next).
|
|
365
|
+
* @param {{users?: string[], roles?: string[]} | null | undefined} review
|
|
366
|
+
* @param {string} userId
|
|
367
|
+
* @param {boolean} reviewed
|
|
368
|
+
* @returns {{users: string[], roles: string[]}}
|
|
369
|
+
*/
|
|
370
|
+
export const withReviewedUser = (review, userId, reviewed) => {
|
|
371
|
+
const { users, roles } = readReview({ plaid: { [REVIEW_KEY]: review } });
|
|
372
|
+
const next = users.filter((u) => u !== userId);
|
|
373
|
+
if (reviewed) next.push(userId);
|
|
374
|
+
return { users: next, roles };
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// ---- confidence: which machine values are doubtful ----
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Key of the project's confidence setting under `config.plaid`:
|
|
381
|
+
* `{ threshold: number in [0, 1] }`. A machine value whose provProb is below
|
|
382
|
+
* the threshold is DOUBTFUL, and both apps give it a heavier mark. Producers
|
|
383
|
+
* write provProb calibrated on held-out data (manual, "Provenance"), so one
|
|
384
|
+
* number serves every service on the project.
|
|
385
|
+
*/
|
|
386
|
+
export const CONFIDENCE_KEY = 'confidence';
|
|
387
|
+
|
|
388
|
+
/** The threshold when a project sets none: below it, a calibrated value is more likely wrong than right. */
|
|
389
|
+
export const DEFAULT_CONFIDENCE_THRESHOLD = 0.5;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The project's confidence threshold, or the default when it sets none or
|
|
393
|
+
* stores something that is not a number in [0, 1].
|
|
394
|
+
* @param {object|null} [config] - a project's `config`
|
|
395
|
+
* @returns {number}
|
|
396
|
+
*/
|
|
397
|
+
export const readConfidenceThreshold = (config) => {
|
|
398
|
+
const t = config?.plaid?.[CONFIDENCE_KEY]?.threshold;
|
|
399
|
+
return typeof t === 'number' && t >= 0 && t <= 1 ? t : DEFAULT_CONFIDENCE_THRESHOLD;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The producer's probability for a machine value nobody has reviewed, or
|
|
404
|
+
* null. Null once a person has confirmed or contributed it, since provProb
|
|
405
|
+
* describes the original prediction and not the value as it stands.
|
|
406
|
+
* @param {object|null|undefined} metadata
|
|
407
|
+
* @returns {number|null}
|
|
408
|
+
*/
|
|
409
|
+
export const machineProb = (metadata) => {
|
|
410
|
+
if (provState(metadata) !== PROV_STATES.MACHINE) return null;
|
|
411
|
+
const p = metadata[PROV.probKey];
|
|
412
|
+
return typeof p === 'number' && Number.isFinite(p) ? p : null;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Whether an unreviewed machine value is below the project's threshold.
|
|
417
|
+
* @param {object|null|undefined} metadata
|
|
418
|
+
* @param {number} [threshold] - readConfidenceThreshold(project.config)
|
|
419
|
+
* @returns {boolean}
|
|
420
|
+
*/
|
|
421
|
+
export const isDoubtful = (metadata, threshold = DEFAULT_CONFIDENCE_THRESHOLD) => {
|
|
422
|
+
const p = machineProb(metadata);
|
|
423
|
+
return p != null && p < threshold;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/** The provDetail key of a producer's distribution over a span's or relation's value. */
|
|
427
|
+
export const VALUE_PROBS_KEY = 'valueProbs';
|
|
428
|
+
/** The provDetail key of a producer's distribution over a morpheme's form. */
|
|
429
|
+
export const FORM_PROBS_KEY = 'formProbs';
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The distribution a producer recorded under provDetail[key] (valueProbs by
|
|
433
|
+
* default, formProbs for a morpheme's form), as a {label: probability} map
|
|
434
|
+
* holding only the entries whose number is a probability, or null when there
|
|
435
|
+
* is none.
|
|
436
|
+
* @param {object|null|undefined} metadata
|
|
437
|
+
* @param {string} [key]
|
|
438
|
+
* @returns {Object<string, number>|null}
|
|
439
|
+
*/
|
|
440
|
+
export const readValueProbs = (metadata, key = VALUE_PROBS_KEY) => {
|
|
441
|
+
const raw = metadata?.[PROV.detailKey]?.[key];
|
|
442
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
443
|
+
const entries = Object.entries(raw).filter(([v, p]) => v !== '' && isProbability(p));
|
|
444
|
+
return entries.length ? Object.fromEntries(entries) : null;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// ---- the writer's policy: what a person's writes carry ----
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* What one writer's writes carry and what their review gestures act on,
|
|
451
|
+
* given who they are: `contributorId` is their user id when their work is
|
|
452
|
+
* reviewed (isReviewed), else null for a verifier. Apps derive this once per
|
|
453
|
+
* document and route every human write through it, so the convention's
|
|
454
|
+
* rule 3 has one implementation.
|
|
455
|
+
*
|
|
456
|
+
* @param {string|null} contributorId
|
|
457
|
+
* @returns {{
|
|
458
|
+
* contributorId: string|null,
|
|
459
|
+
* isContributor: boolean,
|
|
460
|
+
* createStamp: Object|null,
|
|
461
|
+
* editStamp: (metadata: Object) => Object|null,
|
|
462
|
+
* valueEditStamp: (metadata: Object, opts?: {changes?: boolean, value?: string, probsKey?: string, prob?: number|null}) => Object|null,
|
|
463
|
+
* repointStamp: (metadata: Object) => Object|null,
|
|
464
|
+
* confirmStamp: (metadata: Object) => Object|null,
|
|
465
|
+
* confirmFragment: () => Object,
|
|
466
|
+
* adoptStamp: (source: string, detail?: Object) => Object,
|
|
467
|
+
* reviewable: (metadata: Object) => boolean,
|
|
468
|
+
* reviewableState: (state: string) => boolean,
|
|
469
|
+
* }}
|
|
470
|
+
*/
|
|
471
|
+
export const writerPolicy = (contributorId = null) => {
|
|
472
|
+
const id = contributorId || null;
|
|
473
|
+
const contributor = id != null;
|
|
474
|
+
// Material this writer's review gestures act on: a verifier reviews
|
|
475
|
+
// machine and contributed material; a contributor reviews machine
|
|
476
|
+
// proposals only, since their own vouching is itself a contribution.
|
|
477
|
+
const reviewable = (metadata) => (contributor ? isMachine(metadata) : needsReview(metadata));
|
|
478
|
+
const reviewableState = (state) =>
|
|
479
|
+
contributor
|
|
480
|
+
? state === PROV_STATES.MACHINE
|
|
481
|
+
: state === PROV_STATES.MACHINE || state === PROV_STATES.CONTRIBUTED;
|
|
482
|
+
const editStamp = (metadata) =>
|
|
483
|
+
contributor ? contributeOnEdit(metadata, id) : verifyOnEdit(metadata);
|
|
484
|
+
const confirmFragment = () => (contributor ? contributeOnEdit(null, id) : PROV_CONFIRMED);
|
|
485
|
+
return Object.freeze({
|
|
486
|
+
contributorId: id,
|
|
487
|
+
isContributor: contributor,
|
|
488
|
+
/** The metadata a NEW entity carries: null for a verifier. */
|
|
489
|
+
createStamp: contributor ? stampContributed(id) : null,
|
|
490
|
+
/**
|
|
491
|
+
* The fragment an EDIT merges over the entity's metadata, or null when
|
|
492
|
+
* there is nothing to merge: a verifier confirms what needs review; a
|
|
493
|
+
* contributor's edit marks the entity contributed, dropping any earlier
|
|
494
|
+
* confirmation.
|
|
495
|
+
*/
|
|
496
|
+
editStamp,
|
|
497
|
+
/**
|
|
498
|
+
* The fragment a person's edit of a VALUE merges: editStamp, plus what
|
|
499
|
+
* the edit means for provProb, the producer's probability for the value
|
|
500
|
+
* the entity holds, however it was entered. A new `value` the producer
|
|
501
|
+
* weighed, found in the entity's own provDetail[probsKey] (valueProbs,
|
|
502
|
+
* or formProbs for a morpheme's form), carries that probability. A
|
|
503
|
+
* `prob` in [0, 1] is used instead when the caller knows the number
|
|
504
|
+
* from elsewhere (a gloss alternative's own valueLinks entry). A new
|
|
505
|
+
* value the producer never weighed removes provProb. A value that
|
|
506
|
+
* `changes` nothing keeps it. provDetail stays, as history. Null when
|
|
507
|
+
* there is nothing to merge.
|
|
508
|
+
*/
|
|
509
|
+
valueEditStamp: (
|
|
510
|
+
metadata,
|
|
511
|
+
{ changes = true, value, probsKey = VALUE_PROBS_KEY, prob = null } = {},
|
|
512
|
+
) => {
|
|
513
|
+
const stamp = editStamp(metadata);
|
|
514
|
+
if (!changes) return stamp;
|
|
515
|
+
const probs = readValueProbs(metadata, probsKey);
|
|
516
|
+
const weighed =
|
|
517
|
+
probs && typeof value === 'string' && Object.hasOwn(probs, value) ? probs[value] : null;
|
|
518
|
+
const p = isProbability(prob) ? prob : weighed;
|
|
519
|
+
if (p != null) return { ...(stamp || {}), [PROV.probKey]: p };
|
|
520
|
+
if (metadata?.[PROV.probKey] == null) return stamp;
|
|
521
|
+
return { ...(stamp || {}), [PROV.probKey]: null };
|
|
522
|
+
},
|
|
523
|
+
/**
|
|
524
|
+
* The fragment that makes a relation whose head a person moved theirs:
|
|
525
|
+
* every provenance key it held removed, and the create stamp merged, so
|
|
526
|
+
* it reads as a relation this writer drew (the convention's "a head
|
|
527
|
+
* re-point yields a person's relation"). Null when there is nothing to
|
|
528
|
+
* merge.
|
|
529
|
+
*/
|
|
530
|
+
repointStamp: (metadata) => {
|
|
531
|
+
const cleared = {};
|
|
532
|
+
for (const k of PROVENANCE_KEYS) if (metadata?.[k] != null) cleared[k] = null;
|
|
533
|
+
const fragment = { ...cleared, ...(contributor ? stampContributed(id) : {}) };
|
|
534
|
+
return Object.keys(fragment).length ? fragment : null;
|
|
535
|
+
},
|
|
536
|
+
/**
|
|
537
|
+
* The fragment an explicit confirm gesture merges, or null when there is
|
|
538
|
+
* nothing for this writer to confirm: PROV_CONFIRMED for a verifier, the
|
|
539
|
+
* contributed stamp for a contributor accepting a machine proposal.
|
|
540
|
+
*/
|
|
541
|
+
confirmStamp: (metadata) => (reviewable(metadata) ? confirmFragment() : null),
|
|
542
|
+
/**
|
|
543
|
+
* The fragment this writer's confirm merges over whatever it confirms,
|
|
544
|
+
* read without an entity: confirmStamp of anything reviewable. For a
|
|
545
|
+
* gesture that confirms many entities with one stamp.
|
|
546
|
+
*/
|
|
547
|
+
confirmFragment,
|
|
548
|
+
/**
|
|
549
|
+
* What an adopted suggestion is written with (a guess or a picked value
|
|
550
|
+
* into an empty cell): born-verified with the suggestion's producer as
|
|
551
|
+
* its source for a verifier; contributed for a contributor, with the
|
|
552
|
+
* producer kept as provDetail.guess. `detail` is the prediction extras.
|
|
553
|
+
*/
|
|
554
|
+
adoptStamp: (source, detail) =>
|
|
555
|
+
contributor
|
|
556
|
+
? { ...stampContributed(id), [PROV.detailKey]: { ...(detail || {}), guess: source } }
|
|
557
|
+
: confirmedInferred(source, { detail }),
|
|
558
|
+
reviewable,
|
|
559
|
+
reviewableState,
|
|
560
|
+
});
|
|
561
|
+
};
|
package/src/replayed.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// A write sent under an Idempotency-Key whose request the server had already
|
|
2
|
+
// run (its first answer was lost, and the client sent it again) is answered
|
|
3
|
+
// from what that first send stored, with `Idempotent-Replayed: true`, and
|
|
4
|
+
// writes nothing. The client resends on its own (retryUnknown), so the header
|
|
5
|
+
// never reaches the caller. It is marked on the answer instead: `replayed`,
|
|
6
|
+
// true and not enumerable, so the answer reads, compares and serializes as it
|
|
7
|
+
// would unmarked. Only an answer with a JSON object or list body can carry it.
|
|
8
|
+
// Imports nothing.
|
|
9
|
+
|
|
10
|
+
/** `value` marked as replayed, when it is an object or a list. */
|
|
11
|
+
export function markReplayed(value) {
|
|
12
|
+
if (value !== null && typeof value === "object") {
|
|
13
|
+
Object.defineProperty(value, "replayed", {
|
|
14
|
+
value: true,
|
|
15
|
+
enumerable: false,
|
|
16
|
+
configurable: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a client answer was replayed from its key's first send.
|
|
24
|
+
* @param {unknown} answer
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export const wasReplayed = (answer) =>
|
|
28
|
+
answer !== null &&
|
|
29
|
+
typeof answer === "object" &&
|
|
30
|
+
Object.getOwnPropertyDescriptor(answer, "replayed")?.enumerable === false &&
|
|
31
|
+
answer.replayed === true;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person who asked for one run: `id` is their account (their email),
|
|
3
|
+
* `name` how they are shown. Both are null when nobody asked.
|
|
4
|
+
* @param {string | null} [id]
|
|
5
|
+
* @param {string | null} [name]
|
|
6
|
+
* @returns {Requester}
|
|
7
|
+
*/
|
|
8
|
+
export function makeRequester(id?: string | null, name?: string | null): Requester;
|
|
9
|
+
/**
|
|
10
|
+
* The requester of one request, from the `requesterId` core put in its data,
|
|
11
|
+
* with their display name read through `client`.
|
|
12
|
+
* @param {{users: {get(id: string): Promise<any>}}} client - A PlaidClient (only `users.get` is used)
|
|
13
|
+
* @param {any} data - The request data a `serve` handler receives
|
|
14
|
+
* @returns {Promise<Requester>}
|
|
15
|
+
*/
|
|
16
|
+
export function requesterOf(client: {
|
|
17
|
+
users: {
|
|
18
|
+
get(id: string): Promise<any>;
|
|
19
|
+
};
|
|
20
|
+
}, data: any): Promise<Requester>;
|
|
21
|
+
/**
|
|
22
|
+
* The person who asked for one service run. Both fields are null when nobody
|
|
23
|
+
* asked.
|
|
24
|
+
* @typedef {object} Requester
|
|
25
|
+
* @property {string | null} id
|
|
26
|
+
* @property {string | null} name
|
|
27
|
+
* @property {(text: string) => string} label `"<text>, requested by <name>"`,
|
|
28
|
+
* or `text` when nobody asked.
|
|
29
|
+
* @property {() => ({id: string, name: string | null} | null)} record
|
|
30
|
+
* `{ id, name }` for a stored report, or null when nobody asked.
|
|
31
|
+
* @property {(detail?: Record<string, any>) => Record<string, any>} detail A
|
|
32
|
+
* machine stamp's `provDetail` with `requestedBy: <id>` added.
|
|
33
|
+
*/
|
|
34
|
+
/** The key a run's stored record names its requester under. */
|
|
35
|
+
export const REQUESTED_BY: "requestedBy";
|
|
36
|
+
/**
|
|
37
|
+
* The person who asked for one service run. Both fields are null when nobody
|
|
38
|
+
* asked.
|
|
39
|
+
*/
|
|
40
|
+
export type Requester = {
|
|
41
|
+
id: string | null;
|
|
42
|
+
name: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* `"<text>, requested by <name>"`,
|
|
45
|
+
* or `text` when nobody asked.
|
|
46
|
+
*/
|
|
47
|
+
label: (text: string) => string;
|
|
48
|
+
/**
|
|
49
|
+
* `{ id, name }` for a stored report, or null when nobody asked.
|
|
50
|
+
*/
|
|
51
|
+
record: () => ({
|
|
52
|
+
id: string;
|
|
53
|
+
name: string | null;
|
|
54
|
+
} | null);
|
|
55
|
+
/**
|
|
56
|
+
* A
|
|
57
|
+
* machine stamp's `provDetail` with `requestedBy: <id>` added.
|
|
58
|
+
*/
|
|
59
|
+
detail: (detail?: Record<string, any>) => Record<string, any>;
|
|
60
|
+
};
|