@larc-iu/plaid-client 0.2.0-alpha.22 → 0.2.0-alpha.26

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.
@@ -0,0 +1,15 @@
1
+ export function isReservedMetadataKey(key: string): boolean;
2
+ export function metadataOps(fragment: object | null | undefined): MetadataOp[];
3
+ export function applyMetadataOps(metadata: object | null | undefined, ops: MetadataOp[]): Record<string, any>;
4
+ export function mergeMetadata(metadata: object | null | undefined, fragment: object | null | undefined): Record<string, any>;
5
+ /**
6
+ * One metadata edit: the body of a metadata PATCH is a list of these.
7
+ */
8
+ export type MetadataOp = {
9
+ op: "set";
10
+ path: string[];
11
+ value: any;
12
+ } | {
13
+ op: "delete";
14
+ path: string[];
15
+ };
@@ -8,6 +8,11 @@
8
8
  import { PLAID_NAMESPACE } from './roles.js';
9
9
  import { PROVENANCE_KEYS } from './provenance.js';
10
10
 
11
+ /**
12
+ * One metadata edit: the body of a metadata PATCH is a list of these.
13
+ * @typedef {{op: "set", path: string[], value: any} | {op: "delete", path: string[]}} MetadataOp
14
+ */
15
+
11
16
  const RESERVED_KEYS = new Set([PLAID_NAMESPACE, ...PROVENANCE_KEYS]);
12
17
 
13
18
  /**
@@ -49,8 +54,8 @@ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
49
54
  * The ops that set each top-level key of `fragment`, deleting a key whose
50
55
  * value is null. This is how a provenance fragment (verifyOnEdit,
51
56
  * contributeOnEdit, ...) is sent as a patch.
52
- * @param {Object|null|undefined} fragment
53
- * @returns {Array<{op: string, path: string[], value?: any}>}
57
+ * @param {object|null|undefined} fragment
58
+ * @returns {MetadataOp[]}
54
59
  */
55
60
  export const metadataOps = (fragment) =>
56
61
  Object.entries(fragment || {}).map(([k, v]) =>
@@ -84,9 +89,9 @@ const applyOp = (node, path, depth, o) => {
84
89
  * set or delete, a path through a non-object, or a first key that is blank,
85
90
  * over 200 characters or holds a control character. The server's caps on
86
91
  * depth, key count, string length and size are not checked here.
87
- * @param {Object|null|undefined} metadata
88
- * @param {Array<{op: string, path: string[], value?: any}>} ops
89
- * @returns {Object}
92
+ * @param {object|null|undefined} metadata
93
+ * @param {MetadataOp[]} ops
94
+ * @returns {Record<string, any>}
90
95
  */
91
96
  export const applyMetadataOps = (metadata, ops) =>
92
97
  (ops || []).reduce((m, o) => {
@@ -111,9 +116,9 @@ export const applyMetadataOps = (metadata, ops) =>
111
116
  * sending those ops, refused where the server would refuse them. How an app
112
117
  * mirrors a provenance stamp (verifyOnEdit, contributeOnEdit, ...) on the row
113
118
  * it shows. Returns a new object.
114
- * @param {Object|null|undefined} metadata
115
- * @param {Object|null|undefined} fragment
116
- * @returns {Object}
119
+ * @param {object|null|undefined} metadata
120
+ * @param {object|null|undefined} fragment
121
+ * @returns {Record<string, any>}
117
122
  */
118
123
  export const mergeMetadata = (metadata, fragment) =>
119
124
  applyMetadataOps(metadata, metadataOps(fragment));
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Fetch every page and return the full flat array of entries, transparently
3
+ * following `nextCursor` until it is null. This is what `.list()` calls.
4
+ *
5
+ * @param {object} client - PlaidClient instance
6
+ * @param {string} path - API path, e.g. '/api/v1/projects'
7
+ * @param {object} [opts]
8
+ * @param {number} [opts.pageSize=1000] - Per-request page size (limit)
9
+ * @param {object} [opts.query={}] - Extra query params (e.g. { 'as-of': asOf })
10
+ * @returns {Promise<Array>} The concatenated entries across all pages
11
+ */
12
+ export function listAll(client: object, path: string, { pageSize, query }?: {
13
+ pageSize?: number;
14
+ query?: object;
15
+ }): Promise<any[]>;
16
+ /**
17
+ * Fetch a single page and return the raw envelope.
18
+ *
19
+ * @param {object} client - PlaidClient instance
20
+ * @param {string} path - API path
21
+ * @param {object} [opts]
22
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
23
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
24
+ * @param {object} [opts.query={}] - Extra query params
25
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
26
+ */
27
+ export function listPage(client: object, path: string, { limit, cursor, query }?: {
28
+ limit?: number;
29
+ cursor?: string;
30
+ query?: object;
31
+ }): Promise<{
32
+ entries: any[];
33
+ nextCursor: (string | null);
34
+ }>;
35
+ /**
36
+ * Async generator yielding each page's entries array in turn, following
37
+ * `nextCursor` until it is null.
38
+ *
39
+ * @param {object} client - PlaidClient instance
40
+ * @param {string} path - API path
41
+ * @param {object} [opts]
42
+ * @param {number} [opts.pageSize=1000] - Per-request page size (limit)
43
+ * @param {object} [opts.query={}] - Extra query params
44
+ * @yields {Array} The entries array for each page
45
+ */
46
+ export function iterPages(client: object, path: string, { pageSize, query }?: {
47
+ pageSize?: number;
48
+ query?: object;
49
+ }): AsyncGenerator<any, void, unknown>;
package/src/pagination.js CHANGED
@@ -39,8 +39,7 @@ function entriesOf(response, path) {
39
39
 
40
40
  /**
41
41
  * Fetch every page and return the full flat array of entries, transparently
42
- * following `nextCursor` until it is null. This is what `.list()` calls so it
43
- * stays backward compatible with the pre-pagination (bare-array) contract.
42
+ * following `nextCursor` until it is null. This is what `.list()` calls.
44
43
  *
45
44
  * @param {object} client - PlaidClient instance
46
45
  * @param {string} path - API path, e.g. '/api/v1/projects'
@@ -51,23 +50,7 @@ function entriesOf(response, path) {
51
50
  */
52
51
  export async function listAll(client, path, { pageSize = 1000, query = {} } = {}) {
53
52
  const all = [];
54
- let cursor = null;
55
- let prevCursor;
56
- do {
57
- const response = await client._request('GET', path, {
58
- queryParams: buildQueryParams(query, pageSize, cursor),
59
- });
60
- all.push(...entriesOf(response, path));
61
- prevCursor = cursor;
62
- cursor = response.nextCursor;
63
- // Guard against a buggy server/proxy that returns a constant non-null
64
- // cursor, which would otherwise loop forever.
65
- if (cursor !== null && cursor !== undefined && cursor === prevCursor) {
66
- throw new Error(
67
- 'Pagination cursor did not advance; aborting to avoid an infinite loop.',
68
- );
69
- }
70
- } while (cursor !== null && cursor !== undefined);
53
+ for await (const entries of iterPages(client, path, { pageSize, query })) all.push(...entries);
71
54
  return all;
72
55
  }
73
56
 
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Provenance: the cross-app convention for distinguishing machine-provided
3
+ * from human-labeled information, expressed as flat metadata keys on
4
+ * annotation entities (spans, relations, vocab links, optionally tokens).
5
+ * Flat scalar keys — the query engine matches flat metadata well, nested
6
+ * objects poorly.
7
+ *
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:
13
+ *
14
+ * human — no prov keys; a verifier made it.
15
+ * machine — { prov: 'inferred', provSource: '<producer>' };
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.
22
+ *
23
+ * The write contract every machine writer must follow:
24
+ * 1. Machine writers may freely replace MACHINE (unverified) material.
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).
50
+ *
51
+ * Producer naming: 'service:<serviceId>' for services (use serviceSource),
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.
55
+ *
56
+ * PREDICTION EXTRAS. A producer may also record how confident it was and
57
+ * what else it considered, in two reserved slots split along the
58
+ * queryability line:
59
+ * - provProb: ONE flat number in [0, 1] — the producer's probability for
60
+ * the value it chose. Flat scalars are what the query engine filters
61
+ * and orders on, so "review the least-confident machine output first"
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).
64
+ * - provDetail: ONE open map for everything else — top-k alternatives or
65
+ * distributions, model name/version, raw scores. Deliberately nested
66
+ * (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
+ * (provConfirmed is exactly the flag to check).
71
+ *
72
+ * Recommended provDetail keys, so consumers (review dashboards, ranked
73
+ * alternatives) can read any producer's output:
74
+ * - value (spans) / form (morpheme tokens): the value the producer
75
+ * wrote, mirroring the entity's own field. The entity may be edited
76
+ * later; this copy is what makes "accepted as-is" vs "corrected"
77
+ * answerable once the entity is verified.
78
+ * - valueProbs / formProbs: a top-k {label: probability} map over the
79
+ * alternatives the producer considered, the chosen label included
80
+ * (provProb, when given, is that label's entry duplicated flat).
81
+ * - model: what made the prediction (a model name, or a tool and its
82
+ * release such as 'stanza==1.11.0'), and version: which version of the
83
+ * writer ran ('<release>+<8 hex>', see the manual, "Provenance"). Every
84
+ * machine writer records both, one of rules alone only version.
85
+ * - anything else (language, adapter, raw scores).
86
+ */
87
+ /** The flat metadata keys of the provenance convention. */
88
+ export const PROV: Readonly<{
89
+ /** 'inferred' when produced by an algorithm/service; absent = human. */
90
+ key: "prov";
91
+ /** Which producer, e.g. 'service:<id>', 'rule:precedent-or-unique'. */
92
+ sourceKey: "provSource";
93
+ /** true once a human confirmed (or edited) the inferred value. */
94
+ confirmedKey: "provConfirmed";
95
+ /** Producer's probability ([0,1]) for the chosen value. Flat = queryable. */
96
+ probKey: "provProb";
97
+ /** Open map of producer extras (alternatives, model version, raw scores). */
98
+ detailKey: "provDetail";
99
+ /** `prov` for machine-made material. Any value but CONTRIBUTED reads as machine. */
100
+ INFERRED: "inferred";
101
+ /** `prov` for a contributor's material. */
102
+ CONTRIBUTED: "contributed";
103
+ }>;
104
+ /**
105
+ * Every provenance key, for a caller that has to name them as a set rather
106
+ * than read one: what a token layer declares under
107
+ * `config.plaid.preserveOnSplit` so a token born of a split inherits the
108
+ * origin of the token it came from, and what a reshape carries by hand.
109
+ *
110
+ * Provenance is the one metadata family Plaid itself owns. The user cannot
111
+ * maintain it, and in a shared project usually cannot even see it, since it
112
+ * may belong to an app they are not using. Everything else on an entity is
113
+ * the user's own content and no structural operation should touch it.
114
+ */
115
+ export const PROVENANCE_KEYS: readonly ("prov" | "provSource" | "provConfirmed" | "provProb" | "provDetail")[];
116
+ /** The four provenance states returned by provState. */
117
+ export const PROV_STATES: Readonly<{
118
+ HUMAN: "human";
119
+ MACHINE: "machine";
120
+ CONTRIBUTED: "contributed";
121
+ VERIFIED: "verified";
122
+ }>;
123
+ export function stampInferred(source: string, { prob, detail }?: {
124
+ prob?: number;
125
+ detail?: any;
126
+ }): {
127
+ provDetail?: any;
128
+ provProb?: number;
129
+ prov: "inferred";
130
+ provSource: string;
131
+ };
132
+ export function confirmedInferred(source: string, extras?: {
133
+ prob?: number;
134
+ detail?: Record<string, any>;
135
+ }): {
136
+ provConfirmed: boolean;
137
+ provDetail?: any;
138
+ provProb?: number;
139
+ prov: "inferred";
140
+ provSource: string;
141
+ };
142
+ /**
143
+ * The metadata fragment that verifies machine-made material:
144
+ * { provConfirmed: true }. Merge (PATCH) it over existing metadata so
145
+ * prov/provSource/provProb/provDetail survive. This is what verifyOnEdit
146
+ * returns for machine-unverified material; use it directly for explicit
147
+ * confirmation gestures (a "confirm" button, confirm-on-touch) where the
148
+ * caller has already checked isMachine.
149
+ */
150
+ export const PROV_CONFIRMED: Readonly<{
151
+ provConfirmed: true;
152
+ }>;
153
+ export function stampContributed(userId: string): {
154
+ prov: "contributed";
155
+ provSource: string;
156
+ };
157
+ export function provState(metadata: object | null | undefined): "human" | "machine" | "contributed" | "verified";
158
+ export function provOrigin(metadata: object | null | undefined): null | "inferred" | "contributed";
159
+ export function isMachine(metadata: object | null | undefined): boolean;
160
+ export function isProtected(metadata: object | null | undefined): boolean;
161
+ export function needsReview(metadata: object | null | undefined): boolean;
162
+ export function verifyOnEdit(metadata: object | null | undefined): Readonly<{
163
+ provConfirmed: true;
164
+ }>;
165
+ export function contributeOnEdit(metadata: object | null | undefined, userId: string): {
166
+ prov: "contributed";
167
+ provSource: string;
168
+ provConfirmed: null;
169
+ };
170
+ export function serviceSource(serviceId: string): string;
171
+ export function userSource(userId: string): string;
172
+ /** The config key, under the `plaid` namespace, holding the review lists. */
173
+ export const REVIEW_KEY: "review";
174
+ export function readReview(config?: object | null): {
175
+ users: string[];
176
+ roles: string[];
177
+ };
178
+ export function projectRole(project: object, userId: string, { isAdmin }?: {
179
+ isAdmin?: boolean;
180
+ }): "maintainer" | "writer" | "reader";
181
+ export function isReviewed(project: object | null | undefined, userId: string | null | undefined, { isAdmin }?: {
182
+ isAdmin?: boolean;
183
+ }): boolean;
184
+ export function withReviewedUser(review: {
185
+ users?: string[];
186
+ roles?: string[];
187
+ } | null | undefined, userId: string, reviewed: boolean): {
188
+ users: string[];
189
+ roles: string[];
190
+ };
191
+ export function writerPolicy(contributorId?: string | null): {
192
+ contributorId: string | null;
193
+ isContributor: boolean;
194
+ createStamp: any | null;
195
+ editStamp: (metadata: any) => any | null;
196
+ confirmStamp: (metadata: any) => any | null;
197
+ adoptStamp: (source: string, detail?: any) => any;
198
+ reviewable: (metadata: any) => boolean;
199
+ reviewableState: (state: string) => boolean;
200
+ };
package/src/provenance.js CHANGED
@@ -151,6 +151,8 @@ export const stampInferred = (source, { prob, detail } = {}) => ({
151
151
  * import carrying upstream human approval, or a guess written only on
152
152
  * explicit user confirmation). Takes the same optional prediction extras
153
153
  * as stampInferred.
154
+ * @param {string} source
155
+ * @param {{prob?: number, detail?: Record<string, any>}} [extras]
154
156
  */
155
157
  export const confirmedInferred = (source, extras = {}) => ({
156
158
  ...stampInferred(source, extras),
@@ -179,7 +181,7 @@ export const stampContributed = (userId) => ({
179
181
 
180
182
  /**
181
183
  * Classify an entity's metadata into one of PROV_STATES.
182
- * @param {Object|null|undefined} metadata
184
+ * @param {object|null|undefined} metadata
183
185
  * @returns {'human'|'machine'|'contributed'|'verified'}
184
186
  */
185
187
  export const provState = (metadata) => {
@@ -193,6 +195,7 @@ export const provState = (metadata) => {
193
195
  * work, PROV.INFERRED for a machine's, PROV.CONTRIBUTED for a contributor's.
194
196
  * What a verified entity's tooltip needs, since provState folds both
195
197
  * origins into 'verified'.
198
+ * @param {object|null|undefined} metadata
196
199
  * @returns {null|'inferred'|'contributed'}
197
200
  */
198
201
  export const provOrigin = (metadata) => {
@@ -203,12 +206,16 @@ export const provOrigin = (metadata) => {
203
206
  /**
204
207
  * Whether this entity is machine-made and not yet verified — the material
205
208
  * machine writers may replace. Complement of isProtected.
209
+ * @param {object|null|undefined} metadata
210
+ * @returns {boolean}
206
211
  */
207
212
  export const isMachine = (metadata) => provState(metadata) === PROV_STATES.MACHINE;
208
213
 
209
214
  /**
210
215
  * Whether a machine writer must leave this entity alone (write-contract
211
216
  * rule 2): true for human-made, contributed and verified material.
217
+ * @param {object|null|undefined} metadata
218
+ * @returns {boolean}
212
219
  */
213
220
  export const isProtected = (metadata) => !isMachine(metadata);
214
221
 
@@ -216,6 +223,8 @@ export const isProtected = (metadata) => !isMachine(metadata);
216
223
  * Whether a verifier still has to look at this entity: machine-made or
217
224
  * contributed, and not yet confirmed. What review UIs mark and sweep, and
218
225
  * what a verifier's confirmation gesture acts on.
226
+ * @param {object|null|undefined} metadata
227
+ * @returns {boolean}
219
228
  */
220
229
  export const needsReview = (metadata) => {
221
230
  const s = provState(metadata);
@@ -226,6 +235,7 @@ export const needsReview = (metadata) => {
226
235
  * The metadata fragment a VERIFIER's edit of this entity should merge in
227
236
  * (write-contract rule 3): PROV_CONFIRMED when the entity needs review,
228
237
  * else null (nothing to do).
238
+ * @param {object|null|undefined} metadata
229
239
  */
230
240
  export const verifyOnEdit = (metadata) => (needsReview(metadata) ? PROV_CONFIRMED : null);
231
241
 
@@ -235,10 +245,11 @@ export const verifyOnEdit = (metadata) => (needsReview(metadata) ? PROV_CONFIRME
235
245
  * so an earlier confirmation is dropped (send it with metadataOps, which
236
246
  * turns a null into a delete, or apply it locally with mergeMetadata). Prediction extras a machine
237
247
  * recorded stay, as history.
238
- * @param {Object|null|undefined} metadata - unused today; the fragment is
248
+ * @param {object|null|undefined} metadata - unused today; the fragment is
239
249
  * the same whatever the entity was, and the parameter keeps the shape of
240
250
  * verifyOnEdit so callers can swap one for the other
241
251
  * @param {string} userId
252
+ * @returns {{prov: "contributed", provSource: string, provConfirmed: null}}
242
253
  */
243
254
  // eslint-disable-next-line no-unused-vars
244
255
  export const contributeOnEdit = (metadata, userId) => ({
@@ -246,10 +257,18 @@ export const contributeOnEdit = (metadata, userId) => ({
246
257
  [PROV.confirmedKey]: null,
247
258
  });
248
259
 
249
- /** Canonical provSource for a service: 'service:<serviceId>'. */
260
+ /**
261
+ * Canonical provSource for a service: 'service:<serviceId>'.
262
+ * @param {string} serviceId
263
+ * @returns {string}
264
+ */
250
265
  export const serviceSource = (serviceId) => `service:${serviceId}`;
251
266
 
252
- /** Canonical provSource for a contributor: 'user:<userId>'. */
267
+ /**
268
+ * Canonical provSource for a contributor: 'user:<userId>'.
269
+ * @param {string} userId
270
+ * @returns {string}
271
+ */
253
272
  export const userSource = (userId) => `user:${userId}`;
254
273
 
255
274
  // ---- review: whose work is reviewed (a project-config norm) ----
@@ -259,7 +278,8 @@ export const REVIEW_KEY = 'review';
259
278
 
260
279
  /**
261
280
  * The project's review lists, normalized: { users: string[], roles: string[] }.
262
- * @param {Object|null|undefined} config - a project's `config`
281
+ * @param {object|null} [config] - a project's `config`
282
+ * @returns {{users: string[], roles: string[]}}
263
283
  */
264
284
  export const readReview = (config) => {
265
285
  const raw = config?.plaid?.[REVIEW_KEY];
@@ -271,7 +291,7 @@ export const readReview = (config) => {
271
291
  * A person's role in a project from its ACL lists: 'maintainer' | 'writer' |
272
292
  * 'reader' | null. An admin with no explicit entry is a maintainer, as the
273
293
  * permission model treats them.
274
- * @param {Object} project - with `maintainers`, `writers`, `readers`
294
+ * @param {object} project - with `maintainers`, `writers`, `readers`
275
295
  * @param {string} userId
276
296
  * @param {Object} [opts]
277
297
  * @param {boolean} [opts.isAdmin]
@@ -287,6 +307,10 @@ export const projectRole = (project, userId, { isAdmin = false } = {}) => {
287
307
  /**
288
308
  * Whether this person's work is reviewed in this project: named in
289
309
  * review.users, or holding a role named in review.roles.
310
+ * @param {object|null|undefined} project
311
+ * @param {string|null|undefined} userId
312
+ * @param {{isAdmin?: boolean}} [opts]
313
+ * @returns {boolean}
290
314
  */
291
315
  export const isReviewed = (project, userId, { isAdmin = false } = {}) => {
292
316
  if (!project || userId == null) return false;
@@ -300,6 +324,10 @@ export const isReviewed = (project, userId, { isAdmin = false } = {}) => {
300
324
  * The review lists with one person added to or removed from `users`
301
325
  * (`roles` untouched). Pure; write the result with
302
326
  * projects.setConfig(id, 'plaid', REVIEW_KEY, next).
327
+ * @param {{users?: string[], roles?: string[]} | null | undefined} review
328
+ * @param {string} userId
329
+ * @param {boolean} reviewed
330
+ * @returns {{users: string[], roles: string[]}}
303
331
  */
304
332
  export const withReviewedUser = (review, userId, reviewed) => {
305
333
  const { users, roles } = readReview({ plaid: { [REVIEW_KEY]: review } });
@@ -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;
package/src/replayed.js CHANGED
@@ -19,7 +19,11 @@ export function markReplayed(value) {
19
19
  return value;
20
20
  }
21
21
 
22
- /** Whether a client answer was replayed from its key's first send. */
22
+ /**
23
+ * Whether a client answer was replayed from its key's first send.
24
+ * @param {unknown} answer
25
+ * @returns {boolean}
26
+ */
23
27
  export const wasReplayed = (answer) =>
24
28
  answer !== null &&
25
29
  typeof answer === "object" &&
@@ -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
+ };
package/src/requester.js CHANGED
@@ -15,12 +15,29 @@
15
15
  // read. When it cannot be read the id stands in for it, and with no requester
16
16
  // at all the label is left as it was and nothing is stored.
17
17
 
18
+ /**
19
+ * The person who asked for one service run. Both fields are null when nobody
20
+ * asked.
21
+ * @typedef {object} Requester
22
+ * @property {string | null} id
23
+ * @property {string | null} name
24
+ * @property {(text: string) => string} label `"<text>, requested by <name>"`,
25
+ * or `text` when nobody asked.
26
+ * @property {() => ({id: string, name: string | null} | null)} record
27
+ * `{ id, name }` for a stored report, or null when nobody asked.
28
+ * @property {(detail?: Record<string, any>) => Record<string, any>} detail A
29
+ * machine stamp's `provDetail` with `requestedBy: <id>` added.
30
+ */
31
+
18
32
  /** The key a run's stored record names its requester under. */
19
- export const REQUESTED_BY = "requestedBy";
33
+ export const REQUESTED_BY = /** @type {const} */ ("requestedBy");
20
34
 
21
35
  /**
22
36
  * The person who asked for one run: `id` is their account (their email),
23
37
  * `name` how they are shown. Both are null when nobody asked.
38
+ * @param {string | null} [id]
39
+ * @param {string | null} [name]
40
+ * @returns {Requester}
24
41
  */
25
42
  export function makeRequester(id = null, name = null) {
26
43
  return {
@@ -38,8 +55,9 @@ export function makeRequester(id = null, name = null) {
38
55
  /**
39
56
  * The requester of one request, from the `requesterId` core put in its data,
40
57
  * with their display name read through `client`.
41
- * @param {object} client - A PlaidClient (only `users.get` is used)
42
- * @param {object} data - The request data a `serve` handler receives
58
+ * @param {{users: {get(id: string): Promise<any>}}} client - A PlaidClient (only `users.get` is used)
59
+ * @param {any} data - The request data a `serve` handler receives
60
+ * @returns {Promise<Requester>}
43
61
  */
44
62
  export async function requesterOf(client, data) {
45
63
  const id = data && typeof data === "object" ? data.requesterId || null : null;
package/src/roles.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The role recorded on a layer's `config`, or null if none.
3
+ * @param {object} [config] a layer's `config` object
4
+ * @returns {string|null}
5
+ */
6
+ export function readRole(config?: object): string | null;
7
+ /**
8
+ * The first layer in `layers` carrying the given role, or null. The single
9
+ * "find a layer by its role" primitive — apps build their named finders
10
+ * (findWordTokenLayer, etc.) on top of this.
11
+ * @template {{config?: object}} T
12
+ * @param {T[] | undefined} layers
13
+ * @param {string} role
14
+ * @returns {T | null}
15
+ */
16
+ export function findByRole<T extends {
17
+ config?: object;
18
+ }>(layers: T[] | undefined, role: string): T | null;
19
+ /**
20
+ * Shared layer-role vocabulary for cross-app interoperability.
21
+ *
22
+ * Apps that share a Plaid project agree on the *substrate* — the text and token
23
+ * layers — by tagging each shared layer with a ROLE under the reserved `plaid`
24
+ * config namespace (`config.plaid.role`, a scalar). Annotations stay private to
25
+ * each app under that app's own namespace. See the Plaid manual, "Layer
26
+ * Interoperability". The role inventory is small and fixed:
27
+ *
28
+ * baseline the primary text layer
29
+ * sentence sentence token layer
30
+ * word orthographic-word token layer (CoNLL-U "token")
31
+ * syntactic-word grammatical words below the word (CoNLL-U "word" / MWT splits)
32
+ * morpheme morpheme token layer
33
+ * time-alignment media-timeline token layer
34
+ *
35
+ * Only these values are understood across apps; an app may store any string but
36
+ * loses interoperability for unknown values.
37
+ */
38
+ /** The reserved config namespace for cross-app conventions. */
39
+ export const PLAID_NAMESPACE: "plaid";
40
+ /** The config key, under `plaid`, holding a layer's role. */
41
+ export const ROLE_KEY: "role";
42
+ /** The fixed role inventory. */
43
+ /**
44
+ * Layer config key naming the metadata keys a token born of a SPLIT inherits
45
+ * from the token it came from. Plaid honors it without knowing what the keys
46
+ * mean; see the manual's `Metadata Preserved Across a Split`. An app declares
47
+ * it so that a split in ANY app, including one that has never heard of these
48
+ * keys, does not silently drop them.
49
+ */
50
+ export const PRESERVE_ON_SPLIT_KEY: "preserveOnSplit";
51
+ /**
52
+ * Layer config key (true): a space typed inside a token of the text's words
53
+ * splits it, the token going on the half sharing more letters with it.
54
+ * Declared on a word layer another app shares, it holds for the whole text.
55
+ * See the manual's `Changing a text's body`.
56
+ */
57
+ export const SPLIT_ON_SPACE_KEY: "splitOnSpace";
58
+ export const ROLES: Readonly<{
59
+ BASELINE: "baseline";
60
+ SENTENCE: "sentence";
61
+ WORD: "word";
62
+ SYNTACTIC_WORD: "syntactic-word";
63
+ MORPHEME: "morpheme";
64
+ TIME_ALIGNMENT: "time-alignment";
65
+ }>;