@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.32

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.
Files changed (73) hide show
  1. package/README.md +39 -0
  2. package/package.json +13 -6
  3. package/src/apiAnnotations.d.ts +508 -0
  4. package/src/apiAnnotations.js +543 -0
  5. package/src/apiDocuments.d.ts +569 -0
  6. package/src/apiDocuments.js +551 -0
  7. package/src/apiLayers.d.ts +369 -0
  8. package/src/apiLayers.js +222 -0
  9. package/src/apiMessages.d.ts +293 -0
  10. package/src/apiMessages.js +265 -0
  11. package/src/apiPeople.d.ts +626 -0
  12. package/src/apiPeople.js +698 -0
  13. package/src/apiProject.d.ts +761 -0
  14. package/src/apiProject.js +774 -0
  15. package/src/apiServer.d.ts +457 -0
  16. package/src/apiServer.js +424 -0
  17. package/src/apiVocab.d.ts +456 -0
  18. package/src/apiVocab.js +425 -0
  19. package/src/batch.d.ts +54 -0
  20. package/src/batch.js +323 -0
  21. package/src/batchRef.d.ts +30 -0
  22. package/src/batchRef.js +128 -0
  23. package/src/client.d.ts +1321 -0
  24. package/src/client.js +743 -0
  25. package/src/codepoint.d.ts +94 -0
  26. package/src/codepoint.js +131 -1
  27. package/src/constraints.d.ts +85 -0
  28. package/src/constraints.js +112 -0
  29. package/src/created.d.ts +2 -0
  30. package/src/created.js +29 -0
  31. package/src/documentLock.d.ts +172 -0
  32. package/src/documentLock.js +384 -0
  33. package/src/entryForms.d.ts +68 -0
  34. package/src/entryForms.js +164 -0
  35. package/src/events.d.ts +60 -0
  36. package/src/events.js +271 -0
  37. package/src/families.d.ts +349 -0
  38. package/src/families.js +396 -0
  39. package/src/http.d.ts +233 -0
  40. package/src/http.js +809 -93
  41. package/src/ids.d.ts +26 -0
  42. package/src/ids.js +96 -0
  43. package/src/index.d.ts +101 -0
  44. package/src/index.js +183 -2105
  45. package/src/metadataOps.d.ts +15 -0
  46. package/src/metadataOps.js +124 -0
  47. package/src/operations.d.ts +36 -0
  48. package/src/operations.js +164 -0
  49. package/src/pagination.d.ts +49 -0
  50. package/src/pagination.js +20 -53
  51. package/src/provenance.d.ts +243 -0
  52. package/src/provenance.js +458 -36
  53. package/src/replayed.d.ts +3 -0
  54. package/src/replayed.js +31 -0
  55. package/src/requester.d.ts +60 -0
  56. package/src/requester.js +74 -0
  57. package/src/roles.d.ts +104 -0
  58. package/src/roles.js +64 -2
  59. package/src/serviceSchema.d.ts +337 -0
  60. package/src/serviceSchema.js +177 -60
  61. package/src/services.d.ts +319 -0
  62. package/src/services.js +742 -58
  63. package/src/sse.d.ts +30 -0
  64. package/src/sse.js +80 -3
  65. package/src/testing.d.ts +128 -0
  66. package/src/testing.js +379 -0
  67. package/src/textEdits.d.ts +59 -0
  68. package/src/textEdits.js +234 -0
  69. package/src/transforms.d.ts +25 -0
  70. package/src/transforms.js +11 -1
  71. package/src/types.d.ts +867 -0
  72. package/src/types.js +663 -0
  73. 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
- * Three states (the PRESENCE of the `prov` key, not its value, is the
9
- * discriminator; absent keys mean "human"):
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 person made it.
14
+ * human — no prov keys; a verifier made it.
12
15
  * machine — { prov: 'inferred', provSource: '<producer>' };
13
- * an algorithm/service made it, no human has vouched.
14
- * verified — same + { provConfirmed: true }; machine-made and a
15
- * human confirmed (or edited) it. provSource stays so
16
- * machine origin remains traceable.
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 verified
21
- * material unless explicitly told to overwrite (an explicit, per-run,
22
- * user-facing opt-in — for services, a declared boolean `overwrite`
23
- * parameter).
24
- * 3. Any human edit of a machine annotation verifies it: the edit also
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, app-specific ids like
29
- * 'gloss:doc-frequency' or 'flex-import' otherwise.
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
- * Both describe the machine's ORIGINAL prediction. They are kept after
43
- * human edits (history is valuable), so a consumer must not present
44
- * provProb as confidence in the CURRENT value once the entity is verified
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
- /** The (currently only) value of `prov`. Presence, not value, decides. */
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
- /** The three provenance states returned by provState. */
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 !== undefined && prob !== null ? { [PROV.probKey]: prob } : {}),
84
- ...(detail ? { [PROV.detailKey]: detail } : {}),
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 {Object|null|undefined} metadata
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
- return metadata[PROV.confirmedKey] ? PROV_STATES.VERIFIED : PROV_STATES.MACHINE;
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 human-verified — the
120
- * material that still needs review, that machine writers may replace, and
121
- * that a human confirmation gesture acts on. Complement of isProtected.
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 human-verified material.
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
- * The metadata fragment a HUMAN edit of this entity should merge in
133
- * (write-contract rule 3): PROV_CONFIRMED when the entity is machine-made
134
- * and unverified, else null (nothing to do).
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 verifyOnEdit = (metadata) => (isMachine(metadata) ? PROV_CONFIRMED : null);
255
+ export const needsReview = (metadata) => {
256
+ const s = provState(metadata);
257
+ return s === PROV_STATES.MACHINE || s === PROV_STATES.CONTRIBUTED;
258
+ };
137
259
 
138
- /** Canonical provSource for a service: 'service:<serviceId>'. */
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
+ };
@@ -0,0 +1,3 @@
1
+ /** `value` marked as replayed, when it is an object or a list. */
2
+ export function markReplayed(value: any): any;
3
+ export function wasReplayed(answer: unknown): boolean;
@@ -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
+ };