Skip to content

Commit 9e367d1

Browse files
committed
chore(plan 64-A): remove dormant Plan-45 system-feature types + resolver
The account-level app-system-out / app-system-in stream pair was superseded by the CMC per-collector channel (:_cmc:apps:<app-code>:[<path>:]collectors:<counterparty-slug> carrying notification/alert-cmc, notification/ack-cmc, consent/scope-request-cmc, consent/scope-update-cmc). Removed: systemFeatureTypes.ts, HDSSystemAlertDef, HDSSystemAckDef, HDSSystemFeature, SystemMessageType, SystemFeatureResolution, resolveStreamSystemFeature, resolveStreamSystemFeatureDetailed, and the matching test block in tests/resolveStream.test.js. Kept: the mode-3 existingStreamRefs[] mechanism on CollectorRequest (generic, can reference any pre-existing stream). Test fixtures renamed from app-system-out/-in to external-stream-a/b. CUSTOM-FIELDS-AND-SYSTEM.md restructured to cover custom-fields only with a historical note pointing at CMC. README features-list bullet updated. Data-model layer (message/system-alert + message/system-ack eventType registrations) left intact so legacy data on existing accounts stays valid for read.
1 parent 4faabf1 commit 9e367d1

11 files changed

Lines changed: 46 additions & 259 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ For the doctor-side form-storage migration, see `doctor-dashboard/app/formSpecAd
2727

2828
## [Unreleased]
2929

30+
### Removed
31+
32+
- **Plan-45 system-feature types + resolver (dormant).** Deleted `ts/appTemplates/systemFeatureTypes.ts` along with `HDSSystemAlertDef`, `HDSSystemAckDef`, `HDSSystemFeature`, `SystemMessageType`, `SystemFeatureResolution` and the `resolveStreamSystemFeature` / `resolveStreamSystemFeatureDetailed` helpers. The account-level `app-system-out` / `app-system-in` stream pair was superseded by the **CMC per-collector channel** (`:_cmc:apps:<app-code>:[<path>:]collectors:<counterparty-slug>`) carrying `notification/alert-cmc`, `notification/ack-cmc`, `consent/scope-request-cmc`, `consent/scope-update-cmc`. The mode-3 `existingStreamRefs[]` mechanism on `CollectorRequest` stays — generic, can reference any pre-existing stream. Data-model layer (`message/system-alert` + `message/system-ack` eventType registrations) left intact so legacy data stays valid. See `_macro/_plans/64-on-the-go-app-testing-atwork/PLAN.md` Phase A.
33+
3034
## [0.11.0] - 2026-05-14
3135

3236
### Plan 58 — `pryv@3.1.0` + `accesses.update` rollout

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Generic toolkit for server and web applications — [Health Data Safe](https://g
1010
## Features
1111

1212
1. **HDS Data Model** — Load and query the [HDS data model](https://github.com/healthdatasafe/data-model): items, streams, authorizations, event types, datasources
13-
2. **App Templates** — Consent-based data collection and sharing (Manager, Collector, Invite, Client flows). Includes the **AppTemplate JSON loader**, **custom-field declarations** (template-private streams via `clientData.hdsCustomField`), and **system-stream messaging** (`message/system-{alert,ack}`). See [`ts/appTemplates/CUSTOM-FIELDS-AND-SYSTEM.md`](./ts/appTemplates/CUSTOM-FIELDS-AND-SYSTEM.md) for the design reference.
13+
2. **App Templates** — Consent-based data collection and sharing (Manager, Collector, Invite, Client flows). Includes the **AppTemplate JSON loader** and **custom-field declarations** (template-private streams via `clientData.hdsCustomField`). See [`ts/appTemplates/CUSTOM-FIELDS-AND-SYSTEM.md`](./ts/appTemplates/CUSTOM-FIELDS-AND-SYSTEM.md) for the design reference. (System messaging now flows through the CMC per-collector channel — see `open-pryv.io/components/cmc/IMPLEMENTERS-GUIDE.md`.)
1414
3. **HDSSettings** — Per-app user settings (locale, theme, timezone, date format, unit system)
1515
4. **HDSProfile** — Account-level profile (display name, avatar, date of birth, sex, country)
1616
5. **Pryv extensions** — Extends [Pryv JS lib](https://github.com/pryv/lib-js) with Socket.io and Monitor support

tests/apptemplatesRequest.test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,22 +165,22 @@ describe('[APRX] appTemplates Requests', function () {
165165
it('[ARES1] should parse existingStreamRefs from content', () => {
166166
const request = new CollectorRequest({
167167
existingStreamRefs: [
168-
{ streamId: 'app-system-out', permissions: ['manage'], purpose: 'system-out' },
169-
{ streamId: 'app-system-in', permissions: ['read'], purpose: 'system-in' }
168+
{ streamId: 'external-stream-a', permissions: ['manage'], purpose: 'example-out' },
169+
{ streamId: 'external-stream-b', permissions: ['read'], purpose: 'example-in' }
170170
]
171171
});
172172
assert.equal(request.existingStreamRefs.length, 2);
173-
assert.equal(request.existingStreamRefs[0].streamId, 'app-system-out');
173+
assert.equal(request.existingStreamRefs[0].streamId, 'external-stream-a');
174174
assert.deepEqual(request.existingStreamRefs[0].permissions, ['manage']);
175-
assert.equal(request.existingStreamRefs[1].streamId, 'app-system-in');
175+
assert.equal(request.existingStreamRefs[1].streamId, 'external-stream-b');
176176
});
177177

178178
it('[ARES2] should serialize existingStreamRefs in content', () => {
179179
const request = new CollectorRequest({});
180-
request.addExistingStreamRef({ streamId: 'app-system-out', permissions: ['manage'] });
180+
request.addExistingStreamRef({ streamId: 'external-stream-a', permissions: ['manage'] });
181181
assert.ok(request.content.existingStreamRefs);
182182
assert.equal(request.content.existingStreamRefs.length, 1);
183-
assert.equal(request.content.existingStreamRefs[0].streamId, 'app-system-out');
183+
assert.equal(request.content.existingStreamRefs[0].streamId, 'external-stream-a');
184184
});
185185

186186
it('[ARES3] should not serialize existingStreamRefs when empty', () => {
@@ -201,7 +201,7 @@ describe('[APRX] appTemplates Requests', function () {
201201
it('[ARES5] should reject empty permissions array', () => {
202202
const request = new CollectorRequest({});
203203
try {
204-
request.addExistingStreamRef({ streamId: 'app-system-out', permissions: [] });
204+
request.addExistingStreamRef({ streamId: 'external-stream-a', permissions: [] });
205205
throw new Error('Should throw error');
206206
} catch (e) {
207207
assert.match(e.message, /permissions must be a non-empty array/);
@@ -211,7 +211,7 @@ describe('[APRX] appTemplates Requests', function () {
211211
it('[ARES6] should reject invalid permission level', () => {
212212
const request = new CollectorRequest({});
213213
try {
214-
request.addExistingStreamRef({ streamId: 'app-system-out', permissions: ['admin'] });
214+
request.addExistingStreamRef({ streamId: 'external-stream-a', permissions: ['admin'] });
215215
throw new Error('Should throw error');
216216
} catch (e) {
217217
assert.match(e.message, /Invalid permission level "admin"/);
@@ -228,7 +228,7 @@ describe('[APRX] appTemplates Requests', function () {
228228

229229
it('[ARES8] should round-trip through setContent', () => {
230230
const r1 = new CollectorRequest({});
231-
r1.addExistingStreamRef({ streamId: 'app-system-out', permissions: ['manage'], purpose: 'system' });
231+
r1.addExistingStreamRef({ streamId: 'external-stream-a', permissions: ['manage'], purpose: 'example' });
232232
const content1 = r1.content;
233233
const r2 = new CollectorRequest(content1);
234234
assert.deepEqual(r2.existingStreamRefs, r1.existingStreamRefs);

tests/loader.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ function validTemplate () {
2525
}
2626
],
2727
existingStreamRefs: [
28-
{ streamId: 'app-system-out', permissions: ['manage'], purpose: 'system-out' },
29-
{ streamId: 'app-system-in', permissions: ['read'], purpose: 'system-in' }
28+
{ streamId: 'external-stream-a', permissions: ['manage'], purpose: 'example-out' },
29+
{ streamId: 'external-stream-b', permissions: ['read'], purpose: 'example-in' }
3030
]
3131
};
3232
}

tests/resolveStream.test.js

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import {
33
buildStreamMap,
44
resolveStreamCustomField,
55
resolveStreamCustomFieldDetailed,
6-
resolveStreamSystemFeature,
7-
resolveStreamSystemFeatureDetailed,
86
streamCustomFieldToVirtualItem
97
} from '../ts/appTemplates/resolveStream.ts';
108

@@ -62,28 +60,6 @@ function tree () {
6260
]
6361
}
6462
]
65-
},
66-
{
67-
id: 'app-system',
68-
parentId: null,
69-
children: [
70-
{
71-
id: 'app-system-out',
72-
parentId: 'app-system',
73-
clientData: {
74-
hdsSystemFeature: {
75-
'message/system-alert': { version: 'v1', levels: ['info', 'warning', 'critical'] }
76-
}
77-
}
78-
},
79-
{
80-
id: 'app-system-in',
81-
parentId: 'app-system',
82-
clientData: {
83-
hdsSystemFeature: { 'message/system-ack': { version: 'v1' } }
84-
}
85-
}
86-
]
8763
}
8864
];
8965
}
@@ -92,9 +68,8 @@ describe('[CFRS] resolveStream', function () {
9268
describe('[CFRS-MAP] buildStreamMap', function () {
9369
it('[CFRS-MAP-1] flattens a nested tree by id', function () {
9470
const map = buildStreamMap(tree());
95-
assert.equal(map.size, 8); // 5 stormm + 3 app-system
71+
assert.equal(map.size, 5);
9672
assert.ok(map.has('stormm-woman-custom-flow'));
97-
assert.ok(map.has('app-system-out'));
9873
});
9974
});
10075

@@ -144,34 +119,6 @@ describe('[CFRS] resolveStream', function () {
144119
});
145120
});
146121

147-
describe('[CFRS-SF] resolveStreamSystemFeature', function () {
148-
it('[CFRS-SF-1] resolves system-alert on app-system-out', function () {
149-
const def = resolveStreamSystemFeature(tree(), 'app-system-out', 'message/system-alert');
150-
assert.ok(def);
151-
assert.equal(def.version, 'v1');
152-
assert.deepEqual(def.levels, ['info', 'warning', 'critical']);
153-
});
154-
155-
it('[CFRS-SF-2] resolves system-ack on app-system-in', function () {
156-
const def = resolveStreamSystemFeature(tree(), 'app-system-in', 'message/system-ack');
157-
assert.ok(def);
158-
assert.equal(def.version, 'v1');
159-
});
160-
161-
it('[CFRS-SF-3] returns null for cross-pair (alert on -in / ack on -out)', function () {
162-
const a = resolveStreamSystemFeature(tree(), 'app-system-in', 'message/system-alert');
163-
const b = resolveStreamSystemFeature(tree(), 'app-system-out', 'message/system-ack');
164-
assert.equal(a, null);
165-
assert.equal(b, null);
166-
});
167-
168-
it('[CFRS-SF-4] detailed variant returns def kind', function () {
169-
const r = resolveStreamSystemFeatureDetailed(tree(), 'app-system-out', 'message/system-alert');
170-
assert.equal(r.kind, 'def');
171-
assert.equal(r.def.version, 'v1');
172-
});
173-
});
174-
175122
describe('[CFRS-VI] streamCustomFieldToVirtualItem', function () {
176123
it('[CFRS-VI-1] converts a note/txt with options to a select-type virtual item', function () {
177124
const item = streamCustomFieldToVirtualItem(tree(), 'stormm-woman-custom-flow', 'note/txt');

ts/appTemplates/CUSTOM-FIELDS-AND-SYSTEM.md

Lines changed: 24 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
# Custom fields & system stream — design reference
1+
# Custom fields — design reference
22

3-
This is the canonical developer/agent reference for two `appTemplates` extensions
4-
that share the same infrastructure:
3+
This is the canonical developer/agent reference for the `appTemplates` custom-field
4+
extension: **template-private fields stored on namespaced streams**, declared in
5+
`clientData.hdsCustomField` rather than baked into the canonical data model so
6+
templates extend the model without polluting it.
57

6-
1. **Custom fields** — template-private fields stored on namespaced streams
7-
2. **System stream** — operator → user alerts and user → operator acks on the
8-
account-level `app-system/*` stream pair
9-
10-
Both are **declared in `clientData`** rather than baked into the canonical data
11-
model, so templates extend the model without polluting it.
8+
> The companion **system-stream** design (Plan 45 account-level `app-system-out` /
9+
> `app-system-in` with `clientData.hdsSystemFeature`) was removed in plan 64
10+
> Phase A — superseded by the **CMC per-collector system channel**
11+
> (`:_cmc:apps:<app-code>:[<path>:]collectors:<counterparty-slug>`) carrying
12+
> `notification/alert-cmc`, `notification/ack-cmc`, `consent/scope-request-cmc`,
13+
> `consent/scope-update-cmc`. See `open-pryv.io/components/cmc/IMPLEMENTERS-GUIDE.md`.
1214
1315
> Companion reading:
1416
> - `data-model/documentation/CUSTOM-FIELDS-AND-SYSTEM.md` — validator side
@@ -30,23 +32,11 @@ Custom fields let a template **provision its own template-private streams** at
3032
acceptance time, declared with everything the form engine and validator need to
3133
treat them as first-class fields *without* a canonical itemDef.
3234

33-
### Why the system stream exists
34-
35-
Operators (e.g. study coordinators, doctors) need a back-channel to push
36-
notifications to participants ("please complete today's questionnaire") and to
37-
receive structured acknowledgements. Chat is correspondent-scoped (per
38-
collector ↔ user pair); system messages are account-scoped (one inbox per user)
39-
because users want a single "alerts" pane.
40-
41-
Plan 25 already shipped the `{app-id}-app/` convention (e.g. `bridge-mira-app-notes`).
42-
The system stream extends that to **per-account** fixtures `app-system-out` /
43-
`app-system-in` carrying `clientData.hdsSystemFeature` declarations.
44-
4535
### The "no canonical model branding" principle
4636

47-
Neither feature mutates `data-model`'s `pack.json`. The data-model only ships:
37+
Custom fields do not mutate `data-model`'s `pack.json`. The data-model only ships:
4838

49-
- Storage-shape `eventTypes` (`note/txt`, `count/generic`, `message/system-alert`, …)
39+
- Storage-shape `eventTypes` (`note/txt`, `count/generic`, …)
5040
- Semantic itemDefs that map onto those eventTypes
5141

5242
Templates carry their own typing inline on the streams they provision. The
@@ -57,14 +47,12 @@ no naming convention check, no regex on stream ids, no central registry.
5747

5848
Plan 25 generalised the bridge-side stream layout (`bridge-mira-app-notes`,
5949
`bridge-mira-app-chat`, …). Plan 45 reuses the same idea for **templates**
60-
(`stormm-woman-custom-flow`) and adds **account-level** fixtures
61-
(`app-system-out`, `app-system-in`) for system messaging.
50+
(`stormm-woman-custom-flow`).
6251

6352
| Layer | Convention | Example |
6453
| ----------- | ----------------------------------- | -------------------------------- |
6554
| Bridge | `{bridge-id}-app-{suffix}` | `bridge-mira-app-notes` |
6655
| Template | `{template-id}-custom-{key}` | `stormm-woman-custom-flow` |
67-
| Account | `app-{feature}-{out\|in}` | `app-system-out`, `app-system-in`|
6856

6957
Naming is **soft / non-load-bearing** — the hyphenated prefixes are conventions
7058
for human readability. The validator and resolver consume `clientData`, never
@@ -153,43 +141,11 @@ The requester's access gains `contribute` on each.
153141

154142
---
155143

156-
## 3. `clientData.hdsSystemFeature` schema
157-
158-
Same shape, different key — keyed by `message/*` impl rather than storage eventType:
159-
160-
```jsonc
161-
// stream { id: 'app-system-out', parentId: 'app-system', clientData: ↓ }
162-
{
163-
"hdsSystemFeature": {
164-
"message/system-alert": {
165-
"version": "v1",
166-
"levels": ["info", "warning", "critical"]
167-
}
168-
}
169-
}
170-
171-
// stream { id: 'app-system-in', parentId: 'app-system', clientData: ↓ }
172-
{
173-
"hdsSystemFeature": {
174-
"message/system-ack": { "version": "v1" }
175-
}
176-
}
177-
```
178-
179-
A descendant stream can override (declare its own def) or opt-out (declare `{}`)
180-
exactly like custom fields.
181-
182-
Future extension types (`message/system-reminder`, `message/access-update-request`)
183-
can be added without breaking existing readers — unknown keys are skipped by the
184-
resolver.
185-
186-
---
187-
188144
## 4. Inheritance semantics (parent-chain walk)
189145

190146
A field-def is resolved by walking from a stream up its parent chain. For each
191147
stream visited, the resolver inspects `clientData.hdsCustomField[eventType]`
192-
(or `hdsSystemFeature[messageType]`) and applies one of three rules:
148+
and applies one of three rules:
193149

194150
| Value on stream | Outcome |
195151
| ----------------------- | -------------------------------------- |
@@ -250,9 +206,8 @@ the form engine's responsibility (per spec §4 / Plan 45 Q6).
250206

251207
## 6. Stream-naming convention is soft / non-load-bearing
252208

253-
The `*-custom-*` infix on template-private streams and the `app-system-*` /
254-
`app-system-in` names on account fixtures are **human-readability conventions**.
255-
The validator and resolver:
209+
The `*-custom-*` infix on template-private streams is a **human-readability
210+
convention**. The validator and resolver:
256211

257212
- Never regex on streamIds.
258213
- Never assume a parent's id from a child's id.
@@ -273,8 +228,6 @@ Located in `ts/appTemplates/resolveStream.ts`:
273228
import {
274229
resolveStreamCustomField,
275230
resolveStreamCustomFieldDetailed,
276-
resolveStreamSystemFeature,
277-
resolveStreamSystemFeatureDetailed,
278231
streamCustomFieldToVirtualItem,
279232
buildStreamMap
280233
} from 'hds-lib';
@@ -289,10 +242,6 @@ explicit opt-out).
289242

290243
Use when you need to distinguish opt-out from missing.
291244

292-
### `resolveStreamSystemFeature(...) / resolveStreamSystemFeatureDetailed(...)`
293-
294-
Same semantics for `clientData.hdsSystemFeature[messageType]`.
295-
296245
### `streamCustomFieldToVirtualItem(streamTreeOrMap, streamId, eventType): VirtualItemDef | null`
297246

298247
Convenience for the form engine. Returns a virtual `ItemDef`-shaped object the
@@ -316,7 +265,7 @@ A `CollectorRequest` references streams via three orthogonal mechanisms:
316265
| ---- | ------------------------------------- | ------------------------------------------------------ |
317266
| 1 | `sections[].itemKeys[]` | Canonical items resolved via `data-model/pack.json`. |
318267
| 2 | `customFields[]` *(provision-new)* | Template-private streams created at acceptance. |
319-
| 3 | `existingStreamRefs[]` | Access asks on pre-existing streams (e.g. `app-system-*`). |
268+
| 3 | `existingStreamRefs[]` | Access asks on pre-existing streams. |
320269

321270
### Mode 2 — sandbox prefix rule
322271

@@ -345,35 +294,18 @@ consent prompt before granting; refusing one ref blocks the entire acceptance
345294
Each ref:
346295
```jsonc
347296
{
348-
"streamId": "app-system-out",
297+
"streamId": "some-account-level-stream",
349298
"permissions": ["manage"],
350-
"purpose": "system-out"
299+
"purpose": "human-readable-purpose"
351300
}
352301
```
353302

354-
The `purpose` field is informational — surfaces in the UI (e.g. "system messaging
355-
to you"). Permissions are Pryv's standard `read | manage | contribute` levels.
303+
The `purpose` field is informational — surfaces in the UI as the consent prompt's
304+
explanation. Permissions are Pryv's standard `read | manage | contribute` levels.
356305

357306
The CollectorClient append-permissions block applies the requested permissions
358307
without `streams.create` calls (the streams already exist).
359308

360-
### How a template declares system support
361-
362-
Templates declare system support **via Mode 3** referencing the canonical
363-
account-level `app-system-out` / `app-system-in` streams. There is no
364-
`features.system` block on the request — the system feature is a Mode-3 ask
365-
like any other cross-app access:
366-
367-
```jsonc
368-
"existingStreamRefs": [
369-
{ "streamId": "app-system-out", "permissions": ["manage"], "purpose": "system-out" },
370-
{ "streamId": "app-system-in", "permissions": ["read"], "purpose": "system-in" }
371-
]
372-
```
373-
374-
This keeps the request shape uniform (`features` is for in-place chat extensions
375-
to a permission, not for cross-app references).
376-
377309
### How the loader enforces the rules
378310

379311
`loader.ts` runs:
@@ -413,9 +345,8 @@ function listTemplatePrivateStreams (streamTree: Pryv.Stream[]) {
413345
}
414346
```
415347

416-
Same approach for `hdsSystemFeature[messageType]`. Result: bridges that wire
417-
custom-field events into external systems (e.g. STORMM data export to REDCap)
418-
work without knowing a template's id ahead of time.
348+
Bridges that wire custom-field events into external systems (e.g. STORMM data
349+
export to REDCap) work without knowing a template's id ahead of time.
419350

420351
---
421352

@@ -475,7 +406,5 @@ through the form engine.
475406
`_plans/47-STORMM-forms-paused/PLAN.md`
476407
- **`data-model/documentation/CUSTOM-FIELDS-AND-SYSTEM.md`** — validator side
477408
(storage-shape eventTypes, parent-chain walk in `data-model/src/items.js`).
478-
- **`data-model/documentation/DESIGN-NOTES.md`** — eventType
479-
`<class>/<implementation>` convention used by the new `message/system-*` types.
480409
- **`hds-lib-js/AGENTS.md`** — agent primer; this file is its detailed
481410
reference for everything `appTemplates`-shaped.

ts/appTemplates/CollectorRequest.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,9 @@ export class CollectorRequest {
319319
get existingStreamRefs (): Array<ExistingStreamRef> { return this.#existingStreamRefs; }
320320

321321
/**
322-
* Request access on a pre-existing stream (e.g. account-level `app-system-out` /
323-
* `app-system-in`). Typing of inner events is owned by the stream's existing
324-
* `clientData`; this CollectorRequest only patches access permissions at acceptance.
322+
* Request access on a pre-existing stream (Plan 45 mode-3). Typing of inner
323+
* events is owned by the stream's existing `clientData`; this CollectorRequest
324+
* only patches access permissions at acceptance.
325325
*/
326326
addExistingStreamRef (ref: ExistingStreamRef) {
327327
if (ref == null || typeof ref !== 'object') throw new HDSLibError('Invalid existingStreamRef', ref);

0 commit comments

Comments
 (0)