@larc-iu/plaid-client 0.2.0-alpha.29 → 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.
Files changed (49) hide show
  1. package/package.json +3 -2
  2. package/src/apiAnnotations.d.ts +508 -0
  3. package/src/apiAnnotations.js +543 -0
  4. package/src/apiDocuments.d.ts +569 -0
  5. package/src/apiDocuments.js +551 -0
  6. package/src/apiLayers.d.ts +369 -0
  7. package/src/apiLayers.js +222 -0
  8. package/src/apiMessages.d.ts +293 -0
  9. package/src/apiMessages.js +265 -0
  10. package/src/apiPeople.d.ts +626 -0
  11. package/src/apiPeople.js +698 -0
  12. package/src/apiProject.d.ts +761 -0
  13. package/src/apiProject.js +774 -0
  14. package/src/apiServer.d.ts +455 -0
  15. package/src/apiServer.js +422 -0
  16. package/src/apiVocab.d.ts +456 -0
  17. package/src/apiVocab.js +425 -0
  18. package/src/batch.d.ts +54 -0
  19. package/src/batch.js +323 -0
  20. package/src/client.d.ts +1321 -0
  21. package/src/client.js +743 -0
  22. package/src/constraints.d.ts +8 -0
  23. package/src/constraints.js +14 -0
  24. package/src/documentLock.d.ts +18 -4
  25. package/src/documentLock.js +43 -9
  26. package/src/entryForms.d.ts +63 -0
  27. package/src/entryForms.js +152 -0
  28. package/src/events.js +2 -1
  29. package/src/families.d.ts +349 -0
  30. package/src/families.js +396 -0
  31. package/src/http.d.ts +39 -18
  32. package/src/http.js +127 -87
  33. package/src/index.d.ts +90 -4557
  34. package/src/index.js +113 -5172
  35. package/src/operations.d.ts +36 -0
  36. package/src/operations.js +164 -0
  37. package/src/provenance.d.ts +47 -4
  38. package/src/provenance.js +168 -12
  39. package/src/roles.d.ts +39 -0
  40. package/src/roles.js +44 -0
  41. package/src/serviceSchema.d.ts +19 -13
  42. package/src/serviceSchema.js +30 -15
  43. package/src/services.d.ts +20 -3
  44. package/src/services.js +45 -8
  45. package/src/sse.js +10 -1
  46. package/src/testing.d.ts +128 -0
  47. package/src/testing.js +379 -0
  48. package/src/types.d.ts +867 -0
  49. package/src/types.js +663 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * End the key frame `frame` of `group`, wherever it is among the frames open:
3
+ * the frame under it, or the one open over it, numbers from then on. A plain
4
+ * function, so a caller that borrows withOperation onto another object
5
+ * (test doubles do) keeps it.
6
+ */
7
+ export function dropFrame(group: any, frame: any): void;
8
+ /**
9
+ * The key for the next keyed request, and the strict-mode stamp it must
10
+ * claim when an earlier run of its operation already sent it. `joins` says
11
+ * whether the request joins the open logical operation.
12
+ * Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
13
+ * and `record(version)` pins what this request claimed, the first time.
14
+ */
15
+ export function nextIdempotencyKey(client: any, joins: any): {
16
+ key: string;
17
+ pin: any;
18
+ record: (version: any) => void;
19
+ };
20
+ /** See PlaidClient.beginOperation. */
21
+ export function beginOperation(client: any, message: any, { groupId, kind, ref, keys, minted }?: {}): any;
22
+ /** See PlaidClient.keySeed. */
23
+ export function keySeed(): {
24
+ seed: string;
25
+ stamps: Map<any, any>;
26
+ };
27
+ /** See PlaidClient.endOperation. */
28
+ export function endOperation(client: any, message: any): Promise<void>;
29
+ /** See PlaidClient.withOperation. */
30
+ export function withOperation(client: any, message: any, fn: any, { kind, ref, groupId, keys, minted }?: {}): Promise<any>;
31
+ /**
32
+ * The kinds an operation may say it is (`?group-kind=`), the server's closed
33
+ * list (plaid.sql.operation-group/kinds). An operation of any other kind is
34
+ * refused when it begins, before it writes anything.
35
+ */
36
+ export const OPERATION_KINDS: string[];
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Logical operations: the audit-log group a run of writes is labelled with,
3
+ * its kind and reference, and the Idempotency-Keys that make a rerun send the
4
+ * same requests. The client's beginOperation, keySeed, endOperation and
5
+ * withOperation (whose notes say what each does) call these, with the client
6
+ * as `client`. The twin of plaid-client-py's operations.py.
7
+ */
8
+
9
+ import { uuidv4, uuidv7 } from "./ids.js";
10
+
11
+ /**
12
+ * The kinds an operation may say it is (`?group-kind=`), the server's closed
13
+ * list (plaid.sql.operation-group/kinds). An operation of any other kind is
14
+ * refused when it begins, before it writes anything.
15
+ */
16
+ export const OPERATION_KINDS = [
17
+ "assistant-plan",
18
+ "service-run",
19
+ "import",
20
+ "bulk-edit",
21
+ "guess-adoption",
22
+ "repair",
23
+ "review",
24
+ ];
25
+
26
+ /**
27
+ * End the key frame `frame` of `group`, wherever it is among the frames open:
28
+ * the frame under it, or the one open over it, numbers from then on. A plain
29
+ * function, so a caller that borrows withOperation onto another object
30
+ * (test doubles do) keeps it.
31
+ */
32
+ export function dropFrame(group, frame) {
33
+ if (!group || !frame || group.frames[0] === frame) return;
34
+ group.frames = group.frames.filter((f) => f !== frame);
35
+ }
36
+
37
+ /**
38
+ * The key for the next keyed request, and the strict-mode stamp it must
39
+ * claim when an earlier run of its operation already sent it. `joins` says
40
+ * whether the request joins the open logical operation.
41
+ * Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
42
+ * and `record(version)` pins what this request claimed, the first time.
43
+ */
44
+ export function nextIdempotencyKey(client, joins) {
45
+ const frame = joins ? client.operationGroup?.frames.at(-1) : null;
46
+ const keys = frame?.keys;
47
+ if (!keys) return { key: uuidv7(), pin: undefined, record: () => {} };
48
+ const n = frame.count;
49
+ frame.count += 1;
50
+ const pinned = keys.stamps.has(n);
51
+ return {
52
+ key: `${keys.seed}.${n}`,
53
+ pin: pinned ? keys.stamps.get(n) : undefined,
54
+ record: (version) => {
55
+ if (!pinned) keys.stamps.set(n, version ?? null);
56
+ },
57
+ };
58
+ }
59
+
60
+ /** See PlaidClient.beginOperation. */
61
+ export function beginOperation(client, message, { groupId, kind, ref, keys, minted } = {}) {
62
+ if (kind != null && !OPERATION_KINDS.includes(String(kind))) {
63
+ throw new Error(
64
+ `Unknown operation kind "${kind}". It is one of: ${OPERATION_KINDS.join(", ")}.`,
65
+ );
66
+ }
67
+ const frame = {
68
+ keys: keys || null,
69
+ count: 0,
70
+ // Any iterable of ids (a Set, an array).
71
+ minted: minted ? new Set(minted) : null,
72
+ depth: 1,
73
+ };
74
+ const open = client.operationGroup;
75
+ client._openedFrame = null;
76
+ if (open) {
77
+ // A nested operation joins the outer one. When it brings its own key
78
+ // seed or ids (a queued edit run inside a longer operation), it numbers
79
+ // the keys in a frame of its own until it ends, then the frame under it
80
+ // numbers again.
81
+ open.depth += 1;
82
+ if (keys || minted) {
83
+ frame.depth = open.depth;
84
+ open.frames.push(frame);
85
+ client._openedFrame = frame;
86
+ }
87
+ return open.id;
88
+ }
89
+ client.operationGroup = {
90
+ id: groupId ? String(groupId) : uuidv4(),
91
+ message: message == null ? null : String(message),
92
+ kind: kind == null ? null : String(kind),
93
+ ref: ref == null ? null : String(ref),
94
+ depth: 1,
95
+ written: false,
96
+ refined: undefined,
97
+ frames: [frame],
98
+ };
99
+ client._openedFrame = frame;
100
+ return client.operationGroup.id;
101
+ }
102
+
103
+ /** See PlaidClient.keySeed. */
104
+ export function keySeed() {
105
+ return { seed: uuidv7(), stamps: new Map() };
106
+ }
107
+
108
+ /** See PlaidClient.endOperation. */
109
+ export async function endOperation(client, message) {
110
+ const group = client.operationGroup;
111
+ if (!group) return;
112
+ if (group.depth > 1) {
113
+ // A frame begun at client depth by hand (beginOperation) ends here. One
114
+ // begun by withOperation is ended by that call itself (_dropFrame).
115
+ const top = group.frames.at(-1);
116
+ if (group.frames.length > 1 && !top.owned && top.depth === group.depth) {
117
+ group.frames.pop();
118
+ }
119
+ group.depth -= 1;
120
+ return;
121
+ }
122
+ client.operationGroup = null;
123
+ const refined = message !== undefined ? message : group.refined;
124
+ if (refined !== undefined && group.written) {
125
+ try {
126
+ await client.operationGroups.update(group.id, refined);
127
+ } catch (e) {
128
+ // 404: the group never materialized server-side (every tagged write
129
+ // failed or a batch was aborted). Nothing to relabel — not an error.
130
+ if (e.status !== 404) throw e;
131
+ }
132
+ }
133
+ }
134
+
135
+ /** See PlaidClient.withOperation. */
136
+ export async function withOperation(client, message, fn, { kind, ref, groupId, keys, minted } = {}) {
137
+ client.beginOperation(message, { kind, ref, groupId, keys, minted });
138
+ const group = client.operationGroup;
139
+ // The key frame client call opened is ended by client call, whatever else
140
+ // began or ended meanwhile, not by whichever end comes at its depth.
141
+ const frame = client._openedFrame;
142
+ if (frame) frame.owned = true;
143
+ const setMessage = (msg) => {
144
+ if (group.depth === 1) group.refined = msg;
145
+ };
146
+ let result;
147
+ try {
148
+ result = await fn(setMessage);
149
+ } catch (e) {
150
+ // A relabel the server refuses (a token scoped to projects may not
151
+ // relabel a group, which names none) must not take the place of the
152
+ // error `fn` threw: the caller answers for that one.
153
+ dropFrame(group, frame);
154
+ try {
155
+ await client.endOperation();
156
+ } catch (relabel) {
157
+ console.warn('The operation was not relabelled:', relabel);
158
+ }
159
+ throw e;
160
+ }
161
+ dropFrame(group, frame);
162
+ await client.endOperation();
163
+ return result;
164
+ }
@@ -60,13 +60,21 @@
60
60
  * the value it chose. Flat scalars are what the query engine filters
61
61
  * and orders on, so "review the least-confident machine output first"
62
62
  * is an ordinary query. Omit it unless you can honestly produce a
63
- * 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.
64
67
  * - provDetail: ONE open map for everything else — top-k alternatives or
65
68
  * distributions, model name/version, raw scores. Deliberately nested
66
69
  * (not queryable); keep it small (top-k, not whole-vocabulary dumps).
67
- * Both describe the machine's ORIGINAL prediction. They are kept after
68
- * human edits (history is valuable), so a consumer must not present
69
- * 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
70
78
  * (provConfirmed is exactly the flag to check).
71
79
  *
72
80
  * Recommended provDetail keys, so consumers (review dashboards, ranked
@@ -78,6 +86,12 @@
78
86
  * - valueProbs / formProbs: a top-k {label: probability} map over the
79
87
  * alternatives the producer considered, the chosen label included
80
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.
81
95
  * - model: what made the prediction (a model name, or a tool and its
82
96
  * release such as 'stanza==1.11.0'), and version: which version of the
83
97
  * writer ran ('<release>+<8 hex>', see the manual, "Provenance"). Every
@@ -175,6 +189,7 @@ export function readReview(config?: object | null): {
175
189
  users: string[];
176
190
  roles: string[];
177
191
  };
192
+ export function reviewsAnyone(config?: object | null): boolean;
178
193
  export function projectRole(project: object, userId: string, { isAdmin }?: {
179
194
  isAdmin?: boolean;
180
195
  }): "maintainer" | "writer" | "reader";
@@ -188,12 +203,40 @@ export function withReviewedUser(review: {
188
203
  users: string[];
189
204
  roles: string[];
190
205
  };
206
+ /**
207
+ * Key of the project's confidence setting under `config.plaid`:
208
+ * `{ threshold: number in [0, 1] }`. A machine value whose provProb is below
209
+ * the threshold is DOUBTFUL, and both apps give it a heavier mark. Producers
210
+ * write provProb calibrated on held-out data (manual, "Provenance"), so one
211
+ * number serves every service on the project.
212
+ */
213
+ export const CONFIDENCE_KEY: "confidence";
214
+ /** The threshold when a project sets none: below it, a calibrated value is more likely wrong than right. */
215
+ export const DEFAULT_CONFIDENCE_THRESHOLD: 0.5;
216
+ export function readConfidenceThreshold(config?: object | null): number;
217
+ export function machineProb(metadata: object | null | undefined): number | null;
218
+ export function isDoubtful(metadata: object | null | undefined, threshold?: number): boolean;
219
+ /** The provDetail key of a producer's distribution over a span's or relation's value. */
220
+ export const VALUE_PROBS_KEY: "valueProbs";
221
+ /** The provDetail key of a producer's distribution over a morpheme's form. */
222
+ export const FORM_PROBS_KEY: "formProbs";
223
+ export function readValueProbs(metadata: object | null | undefined, key?: string): {
224
+ [x: string]: number;
225
+ } | null;
191
226
  export function writerPolicy(contributorId?: string | null): {
192
227
  contributorId: string | null;
193
228
  isContributor: boolean;
194
229
  createStamp: any | null;
195
230
  editStamp: (metadata: any) => any | null;
231
+ valueEditStamp: (metadata: any, opts?: {
232
+ changes?: boolean;
233
+ value?: string;
234
+ probsKey?: string;
235
+ prob?: number | null;
236
+ }) => any | null;
237
+ repointStamp: (metadata: any) => any | null;
196
238
  confirmStamp: (metadata: any) => any | null;
239
+ confirmFragment: () => any;
197
240
  adoptStamp: (source: string, detail?: any) => any;
198
241
  reviewable: (metadata: any) => boolean;
199
242
  reviewableState: (state: string) => boolean;
package/src/provenance.js CHANGED
@@ -60,13 +60,21 @@
60
60
  * the value it chose. Flat scalars are what the query engine filters
61
61
  * and orders on, so "review the least-confident machine output first"
62
62
  * is an ordinary query. Omit it unless you can honestly produce a
63
- * 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.
64
67
  * - provDetail: ONE open map for everything else — top-k alternatives or
65
68
  * distributions, model name/version, raw scores. Deliberately nested
66
69
  * (not queryable); keep it small (top-k, not whole-vocabulary dumps).
67
- * Both describe the machine's ORIGINAL prediction. They are kept after
68
- * human edits (history is valuable), so a consumer must not present
69
- * 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
70
78
  * (provConfirmed is exactly the flag to check).
71
79
  *
72
80
  * Recommended provDetail keys, so consumers (review dashboards, ranked
@@ -78,6 +86,12 @@
78
86
  * - valueProbs / formProbs: a top-k {label: probability} map over the
79
87
  * alternatives the producer considered, the chosen label included
80
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.
81
95
  * - model: what made the prediction (a model name, or a tool and its
82
96
  * release such as 'stanza==1.11.0'), and version: which version of the
83
97
  * writer ran ('<release>+<8 hex>', see the manual, "Provenance"). Every
@@ -130,20 +144,32 @@ export const PROV_STATES = Object.freeze({
130
144
  VERIFIED: 'verified',
131
145
  });
132
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
+
133
155
  /**
134
156
  * The metadata fragment a machine writer merges into everything it creates.
135
157
  * @param {string} source - producer id, e.g. serviceSource(serviceId)
136
158
  * @param {Object} [extras] - optional prediction extras:
137
159
  * @param {number} [extras.prob] - probability in [0,1] for the chosen value
138
- * (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.
139
162
  * @param {Object} [extras.detail] - open map of producer extras (top-k
140
163
  * alternatives, model version, raw scores; keep it small)
141
164
  */
142
165
  export const stampInferred = (source, { prob, detail } = {}) => ({
143
166
  [PROV.key]: PROV.INFERRED,
144
167
  [PROV.sourceKey]: source,
145
- ...(prob !== undefined && prob !== null ? { [PROV.probKey]: prob } : {}),
146
- ...(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
+ : {}),
147
173
  });
148
174
 
149
175
  /**
@@ -287,6 +313,18 @@ export const readReview = (config) => {
287
313
  return { users: list(raw?.users), roles: list(raw?.roles) };
288
314
  };
289
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
+
290
328
  /**
291
329
  * A person's role in a project from its ACL lists: 'maintainer' | 'writer' |
292
330
  * 'reader' | null. An admin with no explicit entry is a maintainer, as the
@@ -336,6 +374,76 @@ export const withReviewedUser = (review, userId, reviewed) => {
336
374
  return { users: next, roles };
337
375
  };
338
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
+
339
447
  // ---- the writer's policy: what a person's writes carry ----
340
448
 
341
449
  /**
@@ -351,7 +459,10 @@ export const withReviewedUser = (review, userId, reviewed) => {
351
459
  * isContributor: boolean,
352
460
  * createStamp: Object|null,
353
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,
354
464
  * confirmStamp: (metadata: Object) => Object|null,
465
+ * confirmFragment: () => Object,
355
466
  * adoptStamp: (source: string, detail?: Object) => Object,
356
467
  * reviewable: (metadata: Object) => boolean,
357
468
  * reviewableState: (state: string) => boolean,
@@ -368,6 +479,9 @@ export const writerPolicy = (contributorId = null) => {
368
479
  contributor
369
480
  ? state === PROV_STATES.MACHINE
370
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);
371
485
  return Object.freeze({
372
486
  contributorId: id,
373
487
  isContributor: contributor,
@@ -379,16 +493,58 @@ export const writerPolicy = (contributorId = null) => {
379
493
  * contributor's edit marks the entity contributed, dropping any earlier
380
494
  * confirmation.
381
495
  */
382
- editStamp: (metadata) => (contributor ? contributeOnEdit(metadata, id) : verifyOnEdit(metadata)),
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
+ },
383
536
  /**
384
537
  * The fragment an explicit confirm gesture merges, or null when there is
385
538
  * nothing for this writer to confirm: PROV_CONFIRMED for a verifier, the
386
539
  * contributed stamp for a contributor accepting a machine proposal.
387
540
  */
388
- confirmStamp: (metadata) => {
389
- if (!reviewable(metadata)) return null;
390
- return contributor ? contributeOnEdit(metadata, id) : PROV_CONFIRMED;
391
- },
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,
392
548
  /**
393
549
  * What an adopted suggestion is written with (a guess or a picked value
394
550
  * into an empty cell): born-verified with the suggestion's producer as
package/src/roles.d.ts CHANGED
@@ -1,9 +1,27 @@
1
+ /**
2
+ * Whether a project records research telemetry: `config.plaid.research.telemetry`
3
+ * is exactly true.
4
+ * @param {object} [config] a project's `config` object
5
+ * @returns {boolean}
6
+ */
7
+ export function readResearchTelemetry(config?: object): boolean;
1
8
  /**
2
9
  * The role recorded on a layer's `config`, or null if none.
3
10
  * @param {object} [config] a layer's `config` object
4
11
  * @returns {string|null}
5
12
  */
6
13
  export function readRole(config?: object): string | null;
14
+ /**
15
+ * The ignored-tokens rule on a word layer's `config` (see
16
+ * `IGNORED_TOKENS_KEY`), or null if none. The one reader of the key in JS.
17
+ * @param {object} [config] a token layer's `config` object
18
+ * @returns {{type: string, whitelist?: string[], blacklist?: string[]}|null}
19
+ */
20
+ export function readIgnoredTokens(config?: object): {
21
+ type: string;
22
+ whitelist?: string[];
23
+ blacklist?: string[];
24
+ } | null;
7
25
  /**
8
26
  * The first layer in `layers` carrying the given role, or null. The single
9
27
  * "find a layer by its role" primitive — apps build their named finders
@@ -55,6 +73,27 @@ export const PRESERVE_ON_SPLIT_KEY: "preserveOnSplit";
55
73
  * See the manual's `Changing a text's body`.
56
74
  */
57
75
  export const SPLIT_ON_SPACE_KEY: "splitOnSpace";
76
+ /**
77
+ * Word layer config key holding the ignored-tokens rule,
78
+ * `{type, whitelist, blacklist}`: under `type: 'unicodePunctuation'` a word made
79
+ * only of punctuation is ignored and each one-character entry of `whitelist` is
80
+ * a letter, under `type: 'blacklist'` the entries of `blacklist` are the words
81
+ * ignored and its one-character entries the only characters that part words.
82
+ * Plaid's text rules and the built-in tokenizer read it. See the manual's
83
+ * `Changing a text's body`.
84
+ */
85
+ export const IGNORED_TOKENS_KEY: "ignoredTokens";
86
+ /**
87
+ * Project config key (false to hide): whether the project's tartan shows beside
88
+ * its name. Absent means shown.
89
+ */
90
+ export const TARTAN_KEY: "tartan";
91
+ /**
92
+ * Project config key holding the research settings, `{telemetry}`. Client
93
+ * events are recorded only while `telemetry` is true. See the manual's
94
+ * `Research telemetry`.
95
+ */
96
+ export const RESEARCH_KEY: "research";
58
97
  export const ROLES: Readonly<{
59
98
  BASELINE: "baseline";
60
99
  SENTENCE: "sentence";
package/src/roles.js CHANGED
@@ -42,6 +42,40 @@ export const PRESERVE_ON_SPLIT_KEY = 'preserveOnSplit';
42
42
  */
43
43
  export const SPLIT_ON_SPACE_KEY = 'splitOnSpace';
44
44
 
45
+ /**
46
+ * Word layer config key holding the ignored-tokens rule,
47
+ * `{type, whitelist, blacklist}`: under `type: 'unicodePunctuation'` a word made
48
+ * only of punctuation is ignored and each one-character entry of `whitelist` is
49
+ * a letter, under `type: 'blacklist'` the entries of `blacklist` are the words
50
+ * ignored and its one-character entries the only characters that part words.
51
+ * Plaid's text rules and the built-in tokenizer read it. See the manual's
52
+ * `Changing a text's body`.
53
+ */
54
+ export const IGNORED_TOKENS_KEY = 'ignoredTokens';
55
+
56
+ /**
57
+ * Project config key (false to hide): whether the project's tartan shows beside
58
+ * its name. Absent means shown.
59
+ */
60
+ export const TARTAN_KEY = 'tartan';
61
+
62
+ /**
63
+ * Project config key holding the research settings, `{telemetry}`. Client
64
+ * events are recorded only while `telemetry` is true. See the manual's
65
+ * `Research telemetry`.
66
+ */
67
+ export const RESEARCH_KEY = 'research';
68
+
69
+ /**
70
+ * Whether a project records research telemetry: `config.plaid.research.telemetry`
71
+ * is exactly true.
72
+ * @param {object} [config] a project's `config` object
73
+ * @returns {boolean}
74
+ */
75
+ export function readResearchTelemetry(config) {
76
+ return config?.[PLAID_NAMESPACE]?.[RESEARCH_KEY]?.telemetry === true;
77
+ }
78
+
45
79
  export const ROLES = Object.freeze({
46
80
  BASELINE: 'baseline',
47
81
  SENTENCE: 'sentence',
@@ -61,6 +95,16 @@ export function readRole(config) {
61
95
  return v == null ? null : v;
62
96
  }
63
97
 
98
+ /**
99
+ * The ignored-tokens rule on a word layer's `config` (see
100
+ * `IGNORED_TOKENS_KEY`), or null if none. The one reader of the key in JS.
101
+ * @param {object} [config] a token layer's `config` object
102
+ * @returns {{type: string, whitelist?: string[], blacklist?: string[]}|null}
103
+ */
104
+ export function readIgnoredTokens(config) {
105
+ return config?.[PLAID_NAMESPACE]?.[IGNORED_TOKENS_KEY] ?? null;
106
+ }
107
+
64
108
  /**
65
109
  * The first layer in `layers` carrying the given role, or null. The single
66
110
  * "find a layer by its role" primitive — apps build their named finders