@larc-iu/plaid-client 0.1.0-alpha.9 → 0.2.0-alpha.1

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,139 @@
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
+ * Three states (the PRESENCE of the `prov` key, not its value, is the
9
+ * discriminator; absent keys mean "human"):
10
+ *
11
+ * human — no prov keys; a person made it.
12
+ * 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.
17
+ *
18
+ * The write contract every machine writer must follow:
19
+ * 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).
26
+ *
27
+ * 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.
30
+ *
31
+ * PREDICTION EXTRAS. A producer may also record how confident it was and
32
+ * what else it considered, in two reserved slots split along the
33
+ * queryability line:
34
+ * - provProb: ONE flat number in [0, 1] — the producer's probability for
35
+ * the value it chose. Flat scalars are what the query engine filters
36
+ * and orders on, so "review the least-confident machine output first"
37
+ * is an ordinary query. Omit it unless you can honestly produce a
38
+ * probability (a raw logprob is NOT one — put it in provDetail).
39
+ * - provDetail: ONE open map for everything else — top-k alternatives or
40
+ * distributions, model name/version, raw scores. Deliberately nested
41
+ * (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
45
+ * (provConfirmed is exactly the flag to check).
46
+ */
47
+
48
+ /** The flat metadata keys of the provenance convention. */
49
+ export const PROV = Object.freeze({
50
+ /** 'inferred' when produced by an algorithm/service; absent = human. */
51
+ key: 'prov',
52
+ /** Which producer, e.g. 'service:<id>', 'rule:precedent-or-unique'. */
53
+ sourceKey: 'provSource',
54
+ /** true once a human confirmed (or edited) the inferred value. */
55
+ confirmedKey: 'provConfirmed',
56
+ /** Producer's probability ([0,1]) for the chosen value. Flat = queryable. */
57
+ probKey: 'provProb',
58
+ /** Open map of producer extras (alternatives, model version, raw scores). */
59
+ detailKey: 'provDetail',
60
+ /** The (currently only) value of `prov`. Presence, not value, decides. */
61
+ INFERRED: 'inferred',
62
+ });
63
+
64
+ /** The three provenance states returned by provState. */
65
+ export const PROV_STATES = Object.freeze({
66
+ HUMAN: 'human',
67
+ MACHINE: 'machine',
68
+ VERIFIED: 'verified',
69
+ });
70
+
71
+ /**
72
+ * The metadata fragment a machine writer merges into everything it creates.
73
+ * @param {string} source - producer id, e.g. serviceSource(serviceId)
74
+ * @param {Object} [extras] - optional prediction extras:
75
+ * @param {number} [extras.prob] - probability in [0,1] for the chosen value
76
+ * (omit unless it honestly is one — raw scores go in detail)
77
+ * @param {Object} [extras.detail] - open map of producer extras (top-k
78
+ * alternatives, model version, raw scores; keep it small)
79
+ */
80
+ export const stampInferred = (source, { prob, detail } = {}) => ({
81
+ [PROV.key]: PROV.INFERRED,
82
+ [PROV.sourceKey]: source,
83
+ ...(prob !== undefined && prob !== null ? { [PROV.probKey]: prob } : {}),
84
+ ...(detail ? { [PROV.detailKey]: detail } : {}),
85
+ });
86
+
87
+ /**
88
+ * The fragment for machine-made material that is born verified (e.g. an
89
+ * import carrying upstream human approval, or a guess written only on
90
+ * explicit user confirmation). Takes the same optional prediction extras
91
+ * as stampInferred.
92
+ */
93
+ export const confirmedInferred = (source, extras = {}) => ({
94
+ ...stampInferred(source, extras),
95
+ [PROV.confirmedKey]: true,
96
+ });
97
+
98
+ /**
99
+ * The metadata fragment that verifies machine-made material:
100
+ * { provConfirmed: true }. Merge (PATCH) it over existing metadata so
101
+ * prov/provSource/provProb/provDetail survive. This is what verifyOnEdit
102
+ * returns for machine-unverified material; use it directly for explicit
103
+ * confirmation gestures (a "confirm" button, confirm-on-touch) where the
104
+ * caller has already checked isMachine.
105
+ */
106
+ export const PROV_CONFIRMED = Object.freeze({ [PROV.confirmedKey]: true });
107
+
108
+ /**
109
+ * Classify an entity's metadata into one of PROV_STATES.
110
+ * @param {Object|null|undefined} metadata
111
+ * @returns {'human'|'machine'|'verified'}
112
+ */
113
+ export const provState = (metadata) => {
114
+ if (!metadata || metadata[PROV.key] == null) return PROV_STATES.HUMAN;
115
+ return metadata[PROV.confirmedKey] ? PROV_STATES.VERIFIED : PROV_STATES.MACHINE;
116
+ };
117
+
118
+ /**
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.
122
+ */
123
+ export const isMachine = (metadata) => provState(metadata) === PROV_STATES.MACHINE;
124
+
125
+ /**
126
+ * Whether a machine writer must leave this entity alone (write-contract
127
+ * rule 2): true for human-made and human-verified material.
128
+ */
129
+ export const isProtected = (metadata) => !isMachine(metadata);
130
+
131
+ /**
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).
135
+ */
136
+ export const verifyOnEdit = (metadata) => (isMachine(metadata) ? PROV_CONFIRMED : null);
137
+
138
+ /** Canonical provSource for a service: 'service:<serviceId>'. */
139
+ export const serviceSource = (serviceId) => `service:${serviceId}`;
@@ -33,14 +33,20 @@
33
33
  * snake_case key round-trips unchanged to a JS UI and back.
34
34
  */
35
35
 
36
- /** The controlled task vocabulary — the fixed integration-point goals. */
36
+ /**
37
+ * The controlled task vocabulary — the fixed integration-point goals.
38
+ *
39
+ * Whatever the task, a service should stamp everything it creates with the
40
+ * provenance convention (see ./provenance.js: stampInferred(serviceSource(id)))
41
+ * so UIs can distinguish machine output until a human verifies it, and should
42
+ * honor the write contract: never destroy human-made or verified material
43
+ * without an explicit `overwrite` parameter. See the manual, "Provenance".
44
+ */
37
45
  export const TASKS = Object.freeze({
38
46
  TOKENIZE: 'tokenize',
39
47
  PARSE: 'parse',
40
48
  TRANSCRIBE: 'transcribe',
41
- /** Create vocab links for unlinked tokens. Services should stamp link
42
- * metadata with the provenance convention ({ prov: 'inferred', provSource:
43
- * 'service:<id>' }) so UIs can distinguish machine links until confirmed. */
49
+ /** Create vocab links for unlinked tokens. */
44
50
  LINK_VOCAB: 'link-vocab',
45
51
  });
46
52
 
package/src/services.js CHANGED
@@ -11,18 +11,34 @@
11
11
  import { transformRequest, transformResponse } from './transforms.js';
12
12
 
13
13
  /**
14
- * Discover the services currently connected to a project — a synchronous GET.
15
- * `timeout` is accepted for back-compat and ignored.
14
+ * Discover the services seen on a project — a synchronous GET. Returns every
15
+ * service ever registered on the project: currently connected ones carry
16
+ * `online: true`; previously-seen offline ones carry `online: false` plus a
17
+ * `lastSeenAt` stamp. Callers that need a service they can actually submit
18
+ * work to should filter on `online`.
16
19
  *
17
20
  * @param {Object} client - PlaidClient instance
18
21
  * @param {string} projectId - Project UUID
19
- * @param {number} [timeout] - Ignored
20
- * @returns {Promise<Array>} [{serviceId, serviceName, description, extras}]
22
+ * @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt}]
21
23
  */
22
- export function discoverServices(client, projectId, timeout) {
24
+ export function discoverServices(client, projectId) {
23
25
  return client._request('GET', `/api/v1/projects/${projectId}/services`);
24
26
  }
25
27
 
28
+ /**
29
+ * Forget a previously-seen (offline) service: removes its row from the
30
+ * project's persistent registry. Maintainer-only; 409 if the service is
31
+ * currently connected (it would just re-register).
32
+ *
33
+ * @param {Object} client - PlaidClient instance
34
+ * @param {string} projectId - Project UUID
35
+ * @param {string} serviceId - Service ID to forget
36
+ * @returns {Promise<void>}
37
+ */
38
+ export function discardService(client, projectId, serviceId) {
39
+ return client._request('DELETE', `/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}`);
40
+ }
41
+
26
42
  /**
27
43
  * Register a service and handle incoming work requests.
28
44
  *
@@ -131,6 +147,16 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
131
147
  * @returns {Promise<any>} The service's result
132
148
  */
133
149
  export function requestService(client, projectId, serviceId, data, timeout = 10000, onProgress) {
150
+ // Propagate an open logical operation (client.beginOperation) to the
151
+ // service: its writes then fold under the requester's audit-log entry
152
+ // (the Python BaseService adopts the id around process_request). Only for a
153
+ // plain-object payload — that's the only shape the service param schema
154
+ // delivers anyway.
155
+ const group = client.operationGroup;
156
+ const payload =
157
+ group && data && typeof data === 'object' && !Array.isArray(data)
158
+ ? { ...data, operationGroup: { id: group.id, message: group.message } }
159
+ : data;
134
160
  return new Promise((resolve, reject) => {
135
161
  const abortController = new AbortController();
136
162
  let settled = false;
@@ -158,7 +184,7 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
158
184
  'Content-Type': 'application/json',
159
185
  'Accept': 'text/event-stream',
160
186
  },
161
- body: JSON.stringify(data === undefined ? null : transformRequest(data)),
187
+ body: JSON.stringify(payload === undefined ? null : transformRequest(payload)),
162
188
  signal: abortController.signal,
163
189
  },
164
190
  );
package/src/sse.js CHANGED
@@ -75,6 +75,13 @@ export function createSSEConnection(client, projectId, onEvent, path) {
75
75
  });
76
76
 
77
77
  if (!response.ok) {
78
+ if (response.status === 409 && streamPath.includes('/services/')) {
79
+ // Service channel rejected: another live instance holds this
80
+ // service-id. The serve() reconnect timer will retry — once the
81
+ // other instance is gone (or its dead channel is reaped) the retry
82
+ // takes over.
83
+ console.warn('Service registration rejected (409): another instance of this service is already connected; will retry');
84
+ }
78
85
  throw new Error(`HTTP ${response.status} ${response.statusText}`);
79
86
  }
80
87