@x12i/memorix-format 2.2.0 → 2.2.2

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.
package/README.md CHANGED
@@ -1,20 +1,25 @@
1
1
  # @x12i/memorix-format
2
2
 
3
- Memorix **product** record format — profiles and conventions on top of
3
+ Memorix **product** record format — open-bucket conventions and migrate tooling layered on
4
4
  [`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format).
5
5
 
6
6
  | Package | Owns |
7
7
  |---|---|
8
8
  | `@x12i/memorix-base-format` | Fixed skeleton: `recordId`, `concept`, `data`, `_…`; unnamed open buckets; ContentTypeMeta |
9
- | `@x12i/memorix-format` | Memorix open-bucket meanings, `associated*` rules, snapshot SCOPE, built-in profiles |
9
+ | `@x12i/memorix-format` | Known open-bucket meanings, `associated*` rules, snapshot SCOPE, built-in profiles, **migrate** CLI |
10
10
 
11
- Product meta-metadata is packaged under [`meta/`](./meta/) (open buckets, content types, association conventions, profiles). Managed-descriptor inventory lives in `meta/inventory/` and is not format law.
11
+ **Object types belong in Catalox descriptors, not here.** Format docs and examples use abstract names (`items`, `groups`, `locations`, `documents`) so domain packs are never confused with format law.
12
12
 
13
- Migrate older envelopes with:
13
+ Product meta lives under [`meta/`](./meta/). Managed-descriptor inventory in `meta/inventory/` is not format law.
14
14
 
15
- ```bash
16
- npx memorix-format migrate ./records/ --out=./clean/
17
- ```
15
+ Normative: [docs/memorix-format-requirements.md](./docs/memorix-format-requirements.md). Narrative: [docs/memorix-format.md](./docs/memorix-format.md).
16
+
17
+ ## Layering
18
+
19
+ 1. Base envelope (identity / core / underscore / open).
20
+ 2. Product open buckets when present: `analytics`, `enrichment`, `insights`, `associated*`.
21
+ 3. **SCOPE:** full open-bucket surface primarily on **snapshots**; other content types keep substance in `.data` unless ContentTypeMeta opts in.
22
+ 4. Readers / human projection **omit** root `_…` and `concept._extIdentifier` by default; keep `recordId` for joins.
18
23
 
19
24
  ## Install
20
25
 
@@ -23,6 +28,27 @@ npm install @x12i/memorix-format
23
28
  # pulls @x12i/memorix-base-format
24
29
  ```
25
30
 
31
+ ## Abstract snapshot example
32
+
33
+ ```json
34
+ {
35
+ "recordId": "rec-item-1",
36
+ "concept": {
37
+ "_extIdentifier": {
38
+ "kind": "sku",
39
+ "sku": "SKU-100",
40
+ "key": "sku:SKU-100"
41
+ },
42
+ "title": "Blue widget"
43
+ },
44
+ "data": { "sku": "SKU-100", "color": "blue" },
45
+ "associatedData": [{ "locationCode": "WH-A", "qty": 12 }],
46
+ "analytics": { "linkedLocations": { "total": 1 } },
47
+ "insights": { "priority": { "level": "high", "confidence": 0.9 } },
48
+ "_system": { "state": "active" }
49
+ }
50
+ ```
51
+
26
52
  ## SDK
27
53
 
28
54
  ```ts
@@ -33,14 +59,12 @@ import {
33
59
  snapshotsContentTypeMeta,
34
60
  } from "@x12i/memorix-format";
35
61
 
36
- // Product validation (base + ASSOC/SCOPE + optional profile)
37
62
  validateMemorixRecord(record, {
38
63
  profile: "snapshots",
39
- objectType: "vulnerabilities",
64
+ objectType: "items",
40
65
  contentType: "snapshots",
41
66
  });
42
67
 
43
- // Base only (no product vocabulary)
44
68
  validateMemorixBaseRecord(record);
45
69
  classifyRootKey("associatedData"); // "open"
46
70
  ```
@@ -48,13 +72,44 @@ classifyRootKey("associatedData"); // "open"
48
72
  ## CLI
49
73
 
50
74
  ```bash
51
- npx memorix-format validate ./record.json --profile=snapshots --object-type=assets
75
+ npx memorix-format validate ./record.json --profile=snapshots --object-type=items
52
76
  npx memorix-format validate ./dir/ --content-type=events
53
77
  npx memorix-format profiles
54
78
  npx memorix-format docs memorix-format-requirements.md
55
79
  ```
56
80
 
57
- ## Migration from 1.0.0
81
+ ## Migrate (recommended transform)
82
+
83
+ Present-tense format only. Transform exports / sample JSON; **live Mongo apply is a separate ops checklist**.
84
+
85
+ ```bash
86
+ # Dry-run (stdout summary; validates after transform)
87
+ npx memorix-format migrate ./export-dir/ --content-type=snapshots
88
+
89
+ # Write cleaned files
90
+ npx memorix-format migrate ./export-dir/ --out=./clean-dir/
91
+
92
+ # Overwrite in place after validation passes
93
+ npx memorix-format migrate ./records.json --in-place
94
+ ```
95
+
96
+ Defaults applied by migrate:
97
+
98
+ - `concept.identifier` → `concept._extIdentifier` (`identifierKey` → `key`)
99
+ - root `identity` / `lifecycle` / `sourceFacts` / `index` removed (lifecycle events folded into `_system.lifecycle`)
100
+ - root `_graphRuns` / `_jobTypeRuns` → under `_system`
101
+ - `_system.state` set to `active` when missing (maps old `lifecycle.status` when present)
102
+
103
+ Writers set `_system.state: "active"` on new records. Pipeline format enforce defaults to **hard** (`MEMORIX_FORMAT_ENFORCE=warn` to soften during cutover).
104
+
105
+ ### Runbook
106
+
107
+ 1. Dry-run against an export or package examples.
108
+ 2. Write `--out` and re-validate.
109
+ 3. Only then apply to Mongo under ops change control (not part of this package’s default workflow).
110
+
111
+ ## Related
58
112
 
59
- See [CHANGELOG.md](./CHANGELOG.md). Envelope validation moved to base-format;
60
- this package no longer pretends product bucket names are part of the base law.
113
+ - Base envelope SOT: [`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format)
114
+ - Associations: [docs/association-format.md](./docs/association-format.md)
115
+ - Changelog: [CHANGELOG.md](./CHANGELOG.md)
@@ -15,9 +15,9 @@ Record-to-record association is always resolved from **snapshot record to snapsh
15
15
  3. Use the matched target snapshot's `recordId` to fetch non-snapshot content (`discovery`, `analysis`, `decisions`, etc.).
16
16
  4. Copy flat `content.data` into `associated<ObjectType><ContentType>` on the source snapshot.
17
17
 
18
- The snapshot resolves the link; embedded content is always fetched **after** snapshot match by `recordId` (or equivalent link field such as `concept.identifier.recordId` or `data.sourceRecordId`).
18
+ The snapshot resolves the link; embedded content is always fetched **after** snapshot match by `recordId` (or equivalent link field such as `concept._extIdentifier.recordId` or `data.sourceRecordId`).
19
19
 
20
- **Never join a source snapshot directly to a non-snapshot content collection by business keys** (for example `vulnerabilities-snapshots.data.assetIp` → `vulnerabilities-discovery.data.assetIp`). Same-object associations still match snapshots first, then fetch content by matched snapshot `recordId`.
20
+ **Never join a source snapshot directly to a non-snapshot content collection by business keys** (for example `findings-snapshots.data.sku` → `findings-discovery.data.sku`). Same-object associations still match snapshots first, then fetch content by matched snapshot `recordId`.
21
21
 
22
22
  ---
23
23
 
@@ -64,7 +64,7 @@ Same copy forms as above, but candidates are **sorted by importance** then **cap
64
64
 
65
65
  Example: `associatedTopVulnerabilitiesAnalysis`
66
66
 
67
- **Importance** comes from the linked object type’s descriptor `importanceIndicator` (e.g. vulnerabilities → `data.severity`), or from the association rule’s `rankBy` override.
67
+ **Importance** comes from the linked object type’s descriptor `importanceIndicator` (e.g. findings → `data.severity`), or from the association rule’s `rankBy` override.
68
68
 
69
69
  **Default cap:** `5` (`DEFAULT_TOP_ASSOCIATED_ITEMS`). Override per rule with `maxItems`.
70
70
 
@@ -80,11 +80,11 @@ Example: `associatedTopVulnerabilitiesAnalysis`
80
80
  "order": "desc"
81
81
  },
82
82
  "match": {
83
- "collection": "vulnerabilities-snapshots",
84
- "joinPaths": ["data.assetIp"]
83
+ "collection": "findings-snapshots",
84
+ "joinPaths": ["data.sku"]
85
85
  },
86
86
  "content": {
87
- "collection": "vulnerabilities-analysis",
87
+ "collection": "findings-analysis",
88
88
  "linkPaths": ["data.sourceRecordId", "recordId"]
89
89
  },
90
90
  "payload": { "source": "target.data", "format": "v2" }
@@ -153,7 +153,7 @@ Rules:
153
153
 
154
154
  Copies a root property from the associated **snapshot** record (not a separate content collection).
155
155
 
156
- Example: `associatedVulnerabilitiesGroupsSnapshotsAnalytics` — copies `analytics` from `vulnerabilities-groups-snapshots`.
156
+ Example: `associatedVulnerabilitiesGroupsSnapshotsAnalytics` — copies `analytics` from `item-groups-snapshots`.
157
157
 
158
158
  ```json
159
159
  {
@@ -175,7 +175,7 @@ When `system.include` is true, the item is wrapped as `{ _system, value }`. Pref
175
175
  - Presets: `.operational/analytics-rollups/presets/*.json`
176
176
  - Runner: `scripts/memorix-2.0-migrate/patches/run-analytics-rollups.mjs`
177
177
 
178
- Use the **same join keys** as association presets (`assetIp`, `name`↔`vulnerabilityName`, CIDR). Writing `analytics` on the target is a rollup; copying another record’s `analytics` into an `associated*SnapshotsAnalytics` array is still an association (form 2 above).
178
+ Use the **same join keys** as association presets (`sku`, `name`↔`vulnerabilityName`, CIDR). Writing `analytics` on the target is a rollup; copying another record’s `analytics` into an `associated*SnapshotsAnalytics` array is still an association (form 2 above).
179
179
 
180
180
  ### 2b. Enrichment association
181
181
 
@@ -183,9 +183,9 @@ Use the **same join keys** as association presets (`assetIp`, `name`↔`vulnerab
183
183
 
184
184
  Copies the linked **snapshot** root `enrichment` object **flat** (same shape as content associations: fields beside optional `_system`, not wrapped in `value`).
185
185
 
186
- `enrichment` is an envelope bucket on snapshots (CVSS, EPSS, KEV, compositeScore, …) — **not** a separate `*-enrichment` content-type collection.
186
+ `enrichment` is an envelope bucket on snapshots (external feed values as received) — **not** a separate `*-enrichment` content-type collection.
187
187
 
188
- Example: `associatedVulnerabilitiesGroupsEnrichment` on `vulnerabilities-snapshots`.
188
+ Example: `associatedVulnerabilitiesGroupsEnrichment` on `findings-snapshots`.
189
189
 
190
190
  ```json
191
191
  {
@@ -210,7 +210,7 @@ Example: `associatedVulnerabilitiesGroupsEnrichment` on `vulnerabilities-snapsho
210
210
  "includeRecordId": false
211
211
  },
212
212
  "target": {
213
- "collection": "vulnerabilities-groups-snapshots",
213
+ "collection": "item-groups-snapshots",
214
214
  "joinPaths": ["data.name"]
215
215
  }
216
216
  }
@@ -219,8 +219,8 @@ Example: `associatedVulnerabilitiesGroupsEnrichment` on `vulnerabilities-snapsho
219
219
  Rules:
220
220
 
221
221
  - Omit the property when no linked snapshot has a non-empty `enrichment` object.
222
- - Do not invent a `vulnerabilities-groups-enrichment` collection for this form.
223
- - Naming helper: `buildAssociatedPropertyName({ form: "enrichment", objectType: "vulnerabilities-groups" })`.
222
+ - Do not invent a `item-groups-enrichment` collection for this form.
223
+ - Naming helper: `buildAssociatedPropertyName({ form: "enrichment", objectType: "item-groups" })`.
224
224
 
225
225
  ### 3. Concepts association
226
226
 
@@ -294,7 +294,7 @@ Strip `_system` when presenting associated content to humans, LLMs, or downstrea
294
294
 
295
295
  ## Identifier and identity matching
296
296
 
297
- Prefer the linked snapshot's `identity` or `concept.identifier`:
297
+ Prefer the linked snapshot's `identity` or `concept._extIdentifier`:
298
298
 
299
299
  ```json
300
300
  { "identity": "group-name:Domain Admins" }
@@ -306,25 +306,25 @@ Prefer the linked snapshot's `identity` or `concept.identifier`:
306
306
  }
307
307
  ```
308
308
 
309
- Canonical location is `concept.identifier`. Root `identifier` / `identity` are accepted when present.
309
+ Canonical location is `concept._extIdentifier`. Root `identifier` / `identity` are accepted when present.
310
310
 
311
- Generate identity strings deterministically: join identifier values in order with `:` (e.g. `assetIp:10.100.18.64`).
311
+ Generate identity strings deterministically: join identifier values in order with `:` (e.g. `sku:10.100.18.64`).
312
312
 
313
- Association rules declare join paths in `sourceJoinPaths` and snapshot match paths in `match.joinPaths` (or legacy `target.joinPaths` for snapshot-only rules). Content fetch rules add `content.linkPaths` that reference the matched snapshot `recordId`. v2 rules should target `concept.identifier` or generated `identity` where possible.
313
+ Association rules declare join paths in `sourceJoinPaths` and snapshot match paths in `match.joinPaths` (or legacy `target.joinPaths` for snapshot-only rules). Content fetch rules add `content.linkPaths` that reference the matched snapshot `recordId`. v2 rules should target `concept._extIdentifier` or generated `identity` where possible.
314
314
 
315
315
  Associator request shape for content associations:
316
316
 
317
317
  ```json
318
318
  {
319
- "sourceJoinPaths": ["data.assetIp"],
319
+ "sourceJoinPaths": ["data.sku"],
320
320
  "match": {
321
- "collection": "vulnerabilities-snapshots",
322
- "joinPaths": ["data.assetIp", "concept.identifier.assetIp", "recordId"]
321
+ "collection": "findings-snapshots",
322
+ "joinPaths": ["data.sku", "concept._extIdentifier.sku", "recordId"]
323
323
  },
324
324
  "content": {
325
- "collection": "vulnerabilities-discovery",
325
+ "collection": "findings-discovery",
326
326
  "contentType": "discovery",
327
- "linkPaths": ["concept.identifier.recordId", "data.sourceRecordId", "recordId"]
327
+ "linkPaths": ["concept._extIdentifier.recordId", "data.sourceRecordId", "recordId"]
328
328
  }
329
329
  }
330
330
  ```
@@ -365,7 +365,7 @@ Process:
365
365
 
366
366
  1. Start from `users-snapshots` record.
367
367
  2. Read join values from `data.securityGroups`.
368
- 3. Match against `security-groups-snapshots` via `concept.identifier` / identity.
368
+ 3. Match against `security-groups-snapshots` via `concept._extIdentifier` / identity.
369
369
  4. Use matched snapshot `recordId` to fetch `security-groups-analysis` records.
370
370
  5. Copy each analysis record's `data` flat into `associatedSecurityGroupsAnalysis`.
371
371
 
@@ -383,7 +383,7 @@ Result:
383
383
  "_system": {
384
384
  "association": {
385
385
  "fromProperty": "data.securityGroups",
386
- "toProperty": "concept.identifier",
386
+ "toProperty": "concept._extIdentifier",
387
387
  "associatedAt": "2026-07-08T09:00:00.000Z"
388
388
  }
389
389
  },
@@ -414,41 +414,41 @@ New associations should be added as registry entries + presets using v2 naming a
414
414
 
415
415
  Snapshot-mediated flow:
416
416
 
417
- 1. Read source `vulnerabilities-snapshots.data.assetIp`.
418
- 2. Match target `vulnerabilities-snapshots` by snapshot paths (`data.assetIp`, `concept.identifier.assetIp`, `identity`, `recordId`).
417
+ 1. Read source `findings-snapshots.data.sku`.
418
+ 2. Match target `findings-snapshots` by snapshot paths (`data.sku`, `concept._extIdentifier.sku`, `identity`, `recordId`).
419
419
  3. Take matched target snapshot `recordId`.
420
- 4. Fetch `vulnerabilities-discovery` / `vulnerabilities-decisions` by record-id link (`concept.identifier.recordId`, `data.sourceRecordId`, or root `recordId`).
420
+ 4. Fetch `findings-discovery` / `findings-decisions` by record-id link (`concept._extIdentifier.recordId`, `data.sourceRecordId`, or root `recordId`).
421
421
  5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
422
422
 
423
423
  | Property | Match collection | Content collection | Join key |
424
424
  |----------|------------------|--------------------|----------|
425
- | `associatedVulnerabilitiesDiscovery` | `vulnerabilities-snapshots` | `vulnerabilities-discovery` | `data.assetIp` → snapshot → `recordId` |
426
- | `associatedVulnerabilitiesDecisions` | `vulnerabilities-snapshots` | `vulnerabilities-decisions` | `data.assetIp` → snapshot → `recordId` |
425
+ | `associatedVulnerabilitiesDiscovery` | `findings-snapshots` | `findings-discovery` | `data.sku` → snapshot → `recordId` |
426
+ | `associatedVulnerabilitiesDecisions` | `findings-snapshots` | `findings-decisions` | `data.sku` → snapshot → `recordId` |
427
427
 
428
- Target source collection: `vulnerabilities-snapshots` (`MEMORIX_EVENTS_DB`).
428
+ Target source collection: `findings-snapshots` (`MEMORIX_EVENTS_DB`).
429
429
 
430
430
  Refresh rule keys:
431
431
 
432
- - `vulnerabilities.vulnerabilities.discovery.by-assetIp`
433
- - `vulnerabilities.vulnerabilities.decisions.by-assetIp`
432
+ - `findings.findings.discovery.by-sku`
433
+ - `findings.findings.decisions.by-sku`
434
434
 
435
435
  ### Pilot: asset-side vulnerability analysis (v2)
436
436
 
437
- Snapshot-mediated flow (source is **assets**, not vulnerabilities):
437
+ Snapshot-mediated flow (source is **items**, not findings):
438
438
 
439
- 1. Read source `assets-snapshots.data.assetIps` (also `data.assetIp`, `data.ip_address`).
440
- 2. Match `vulnerabilities-snapshots` by `data.assetIp` / `concept.identifier.assetIp`.
439
+ 1. Read source `items-snapshots.data.skus` (also `data.sku`, `data.ip_address`).
440
+ 2. Match `findings-snapshots` by `data.sku` / `concept._extIdentifier.sku`.
441
441
  3. Take matched snapshot `recordId`.
442
- 4. Fetch `vulnerabilities-analysis` by record-id link.
442
+ 4. Fetch `findings-analysis` by record-id link.
443
443
  5. Copy flat `content.data` into `associatedVulnerabilitiesAnalysis` on the asset snapshot.
444
444
 
445
445
  | Property | Match collection | Content collection | Join key |
446
446
  |----------|------------------|--------------------|----------|
447
- | `associatedVulnerabilitiesAnalysis` | `vulnerabilities-snapshots` | `vulnerabilities-analysis` | `data.assetIps` → snapshot `data.assetIp` → `recordId` |
447
+ | `associatedVulnerabilitiesAnalysis` | `findings-snapshots` | `findings-analysis` | `data.skus` → snapshot `data.sku` → `recordId` |
448
448
 
449
- Target source collection: `assets-snapshots` (`MEMORIX_ENTITIES_DB`).
449
+ Target source collection: `items-snapshots` (`MEMORIX_ENTITIES_DB`).
450
450
 
451
- Refresh rule key: `assets.vulnerabilities.analysis.by-assetIp`
451
+ Refresh rule key: `items.findings.analysis.by-sku`
452
452
 
453
453
  ---
454
454
 
@@ -58,7 +58,7 @@ type MemorixSnapshotRecord = {
58
58
 
59
59
  This is the rich, readable, consumption-friendly version of the object.
60
60
 
61
- Other content types under the same object type do not need to carry all of these blocks. Memorix already stores records by object type and content type, for example `assets-snapshots` or `vulnerabilities-snapshots`.
61
+ Other content types under the same object type do not need to carry all of these blocks. Memorix already stores records by object type and content type, for example `items-snapshots` or `findings-snapshots`.
62
62
 
63
63
  So the rule is:
64
64
 
@@ -81,15 +81,15 @@ Not every collection under an object type should look like a full snapshot.
81
81
  For example:
82
82
 
83
83
  ```txt id="evq5gl"
84
- assets-snapshots
85
- assets-events
86
- assets-facts
87
- assets-chunks
88
- assets-history
89
- assets-profiles
84
+ items-snapshots
85
+ items-events
86
+ items-facts
87
+ items-chunks
88
+ items-history
89
+ items-profiles
90
90
  ```
91
91
 
92
- Only `assets-snapshots` needs the full record surface.
92
+ Only `items-snapshots` needs the full record surface.
93
93
 
94
94
  Other content types should have the required Memorix structure, but not unnecessary blocks.
95
95
 
@@ -141,7 +141,7 @@ For a CVE record:
141
141
  ```json id="lqeccc"
142
142
  {
143
143
  "data": {
144
- "cveId": "CVE-2022-29464",
144
+ "docId": "DOC-9",
145
145
  "severity": "critical",
146
146
  "description": "Unrestricted file upload vulnerability in WSO2 products.",
147
147
  "publishedAt": "2022-04-18"
@@ -161,9 +161,9 @@ These are facts about the CVE itself.
161
161
 
162
162
  It is not the record’s native truth, but it is very useful for consumption.
163
163
 
164
- For example, a CVE record may need to show the assets affected by that CVE.
164
+ For example, a CVE record may need to show the items affected by that CVE.
165
165
 
166
- Those assets are not part of the CVE itself, so they should not live in `data`.
166
+ Those items are not part of the CVE itself, so they should not live in `data`.
167
167
 
168
168
  But consumers still need them.
169
169
 
@@ -175,12 +175,12 @@ So they belong in `associatedData`.
175
175
  "affectedAssets": [
176
176
  {
177
177
  "assetName": "prod-api-01",
178
- "assetIp": "10.100.18.64",
178
+ "sku": "10.100.18.64",
179
179
  "recordId": "entity:asset:10.100.18.64"
180
180
  },
181
181
  {
182
182
  "assetName": "prod-db-02",
183
- "assetIp": "10.100.18.65",
183
+ "sku": "10.100.18.65",
184
184
  "recordId": "entity:asset:10.100.18.65"
185
185
  }
186
186
  ]
@@ -188,7 +188,7 @@ So they belong in `associatedData`.
188
188
  }
189
189
  ```
190
190
 
191
- This gives consumers the useful joined view without pretending that affected assets are native CVE fields.
191
+ This gives consumers the useful joined view without pretending that affected items are native CVE fields.
192
192
 
193
193
  ### Rule
194
194
 
@@ -209,15 +209,15 @@ It is used when the record has links to lists of other records, and Memorix want
209
209
  For example:
210
210
 
211
211
  ```txt id="omk77f"
212
- CVE -> affected assets
213
- asset -> related vulnerabilities
212
+ CVE -> affected items
213
+ asset -> related findings
214
214
  document -> exposed users
215
215
  user -> accessible sensitive files
216
216
  group -> member users
217
217
  policy -> affected resources
218
218
  ```
219
219
 
220
- If a CVE is linked to many affected assets, then `analytics.affectedAssets` contains numbers about that list.
220
+ If a CVE is linked to many affected items, then `analytics.affectedAssets` contains numbers about that list.
221
221
 
222
222
  ```json id="uerbc6"
223
223
  {
@@ -285,7 +285,7 @@ Bad:
285
285
  "affectedAssets": [
286
286
  {
287
287
  "assetName": "prod-api-01",
288
- "assetIp": "10.100.18.64"
288
+ "sku": "10.100.18.64"
289
289
  }
290
290
  ]
291
291
  }
@@ -300,7 +300,7 @@ Good:
300
300
  "affectedAssets": [
301
301
  {
302
302
  "assetName": "prod-api-01",
303
- "assetIp": "10.100.18.64"
303
+ "sku": "10.100.18.64"
304
304
  }
305
305
  ]
306
306
  },
@@ -333,15 +333,15 @@ It is the place for judgments, assessments, analysis, scores, conclusions, recom
333
333
  "level": "critical",
334
334
  "score": 92,
335
335
  "confidence": 0.84,
336
- "reason": "Critical CVE affecting production and internet-exposed assets."
336
+ "reason": "Critical CVE affecting production and internet-exposed items."
337
337
  },
338
338
  "priority": {
339
339
  "level": "urgent",
340
- "reason": "Affected assets include production systems."
340
+ "reason": "Affected items include production systems."
341
341
  },
342
342
  "recommendations": [
343
343
  {
344
- "action": "Patch affected assets",
344
+ "action": "Patch affected items",
345
345
  "urgency": "high"
346
346
  }
347
347
  ]
@@ -367,7 +367,7 @@ This is an insight:
367
367
  {
368
368
  "riskScore": 92,
369
369
  "priority": "urgent",
370
- "recommendedAction": "Patch affected assets"
370
+ "recommendedAction": "Patch affected items"
371
371
  }
372
372
  ```
373
373
 
@@ -396,7 +396,7 @@ Example:
396
396
  ```json id="3n6qlf"
397
397
  {
398
398
  "data": {
399
- "cveId": "CVE-2022-29464",
399
+ "docId": "DOC-9",
400
400
  "severity": "critical"
401
401
  },
402
402
 
@@ -404,11 +404,11 @@ Example:
404
404
  "affectedAssets": [
405
405
  {
406
406
  "assetName": "prod-api-01",
407
- "assetIp": "10.100.18.64"
407
+ "sku": "10.100.18.64"
408
408
  },
409
409
  {
410
410
  "assetName": "prod-db-02",
411
- "assetIp": "10.100.18.65"
411
+ "sku": "10.100.18.65"
412
412
  }
413
413
  ]
414
414
  },
@@ -424,7 +424,7 @@ Example:
424
424
  "insights": {
425
425
  "risk": {
426
426
  "level": "critical",
427
- "reason": "The CVE affects production assets, including one internet-exposed asset."
427
+ "reason": "The CVE affects production items, including one internet-exposed asset."
428
428
  }
429
429
  }
430
430
  }
@@ -466,7 +466,7 @@ Example:
466
466
  "target": {
467
467
  "recordId": "entity:asset:10.100.18.64",
468
468
  "target": "entity",
469
- "objectType": "assets"
469
+ "objectType": "items"
470
470
  },
471
471
  "display": {
472
472
  "name": "prod-api-01"
@@ -581,10 +581,10 @@ resolve → bounded runtime context (jobMemory.context)
581
581
 
582
582
  Join paths use the same dot-path convention as descriptors: from document root, typically under `data.*`.
583
583
 
584
- Example operator flow for `assets → subnets`:
584
+ Example operator flow for `items → locations`:
585
585
 
586
586
  ```http
587
- GET /api/hippox/join-discovery?sourceObjectType=assets&targetObjectType=subnets
587
+ GET /api/hippox/join-discovery?sourceObjectType=items&targetObjectType=locations
588
588
  POST /api/hippox/links/simulate
589
589
  POST /api/hippox/links/:id/approve
590
590
  POST /api/hippox/context/resolve
@@ -647,8 +647,8 @@ The full-scale shape should be expected mainly in snapshot collections.
647
647
  Example:
648
648
 
649
649
  ```txt id="ot7c3i"
650
- vulnerabilities-snapshots
651
- assets-snapshots
650
+ findings-snapshots
651
+ items-snapshots
652
652
  users-snapshots
653
653
  documents-snapshots
654
654
  ```
@@ -671,10 +671,10 @@ Other content types should stay smaller and purpose-specific.
671
671
  Example:
672
672
 
673
673
  ```txt id="i0znxj"
674
- vulnerabilities-events
675
- vulnerabilities-facts
676
- vulnerabilities-chunks
677
- vulnerabilities-history
674
+ findings-events
675
+ findings-facts
676
+ findings-chunks
677
+ findings-history
678
678
  ```
679
679
 
680
680
  These may only need:
@@ -705,10 +705,10 @@ Other content types are specialized records.
705
705
  "_id": "667f00000000000000000001",
706
706
 
707
707
  "identity": {
708
- "recordId": "entity:cve:CVE-2022-29464",
708
+ "recordId": "entity:cve:DOC-9",
709
709
  "target": "entity",
710
- "targetId": "entity:cve:CVE-2022-29464",
711
- "objectType": "vulnerabilities",
710
+ "targetId": "entity:cve:DOC-9",
711
+ "objectType": "findings",
712
712
  "contentType": "snapshots",
713
713
  "version": "memorix-record.2.0"
714
714
  },
@@ -722,14 +722,14 @@ Other content types are specialized records.
722
722
 
723
723
  "concept": {
724
724
  "kind": "cve",
725
- "name": "CVE-2022-29464",
726
- "title": "CVE-2022-29464",
725
+ "name": "DOC-9",
726
+ "title": "DOC-9",
727
727
  "summary": "Critical unrestricted file upload vulnerability affecting WSO2 products.",
728
- "keywords": ["CVE-2022-29464", "WSO2", "critical"]
728
+ "keywords": ["DOC-9", "WSO2", "critical"]
729
729
  },
730
730
 
731
731
  "data": {
732
- "cveId": "CVE-2022-29464",
732
+ "docId": "DOC-9",
733
733
  "severity": "critical",
734
734
  "description": "Unrestricted file upload vulnerability affecting WSO2 products.",
735
735
  "publishedAt": "2022-04-18"
@@ -739,12 +739,12 @@ Other content types are specialized records.
739
739
  "affectedAssets": [
740
740
  {
741
741
  "assetName": "prod-api-01",
742
- "assetIp": "10.100.18.64",
742
+ "sku": "10.100.18.64",
743
743
  "recordId": "entity:asset:10.100.18.64"
744
744
  },
745
745
  {
746
746
  "assetName": "prod-db-02",
747
- "assetIp": "10.100.18.65",
747
+ "sku": "10.100.18.65",
748
748
  "recordId": "entity:asset:10.100.18.65"
749
749
  }
750
750
  ]
@@ -768,7 +768,7 @@ Other content types are specialized records.
768
768
  "level": "critical",
769
769
  "score": 92,
770
770
  "confidence": 0.84,
771
- "reason": "The CVE affects production assets, including one internet-exposed asset."
771
+ "reason": "The CVE affects production items, including one internet-exposed asset."
772
772
  },
773
773
  "priority": {
774
774
  "level": "urgent",
@@ -776,7 +776,7 @@ Other content types are specialized records.
776
776
  },
777
777
  "recommendations": [
778
778
  {
779
- "action": "Patch affected assets",
779
+ "action": "Patch affected items",
780
780
  "urgency": "high"
781
781
  }
782
782
  ]
@@ -784,9 +784,9 @@ Other content types are specialized records.
784
784
 
785
785
  "index": {
786
786
  "text": {
787
- "title": "CVE-2022-29464",
787
+ "title": "DOC-9",
788
788
  "summary": "Critical unrestricted file upload vulnerability affecting WSO2 products.",
789
- "keywords": ["CVE-2022-29464", "critical", "prod-api-01", "prod-db-02"]
789
+ "keywords": ["DOC-9", "critical", "prod-api-01", "prod-db-02"]
790
790
  },
791
791
  "facets": {
792
792
  "severity": "critical",
@@ -801,7 +801,7 @@ Other content types are specialized records.
801
801
  "target": {
802
802
  "recordId": "entity:asset:10.100.18.64",
803
803
  "target": "entity",
804
- "objectType": "assets"
804
+ "objectType": "items"
805
805
  },
806
806
  "display": {
807
807
  "name": "prod-api-01"
@@ -815,7 +815,7 @@ Other content types are specialized records.
815
815
  "target": {
816
816
  "recordId": "entity:asset:10.100.18.65",
817
817
  "target": "entity",
818
- "objectType": "assets"
818
+ "objectType": "items"
819
819
  },
820
820
  "display": {
821
821
  "name": "prod-db-02"
@@ -840,7 +840,7 @@ Other content types are specialized records.
840
840
 
841
841
  "_system": {
842
842
  "memorix": {
843
- "entity": "vulnerabilities",
843
+ "entity": "findings",
844
844
  "contentType": "snapshots",
845
845
  "operation": "upsert"
846
846
  },
@@ -911,11 +911,11 @@ full record-level index projections
911
911
  So for example:
912
912
 
913
913
  ```txt id="cpyqdi"
914
- vulnerabilities-snapshots ✅ full record shape
915
- vulnerabilities-events ❌ not full shape
916
- vulnerabilities-facts ❌ not full shape
917
- vulnerabilities-chunks ❌ not full shape
918
- vulnerabilities-history ❌ not full shape
914
+ findings-snapshots ✅ full record shape
915
+ findings-events ❌ not full shape
916
+ findings-facts ❌ not full shape
917
+ findings-chunks ❌ not full shape
918
+ findings-history ❌ not full shape
919
919
  ```
920
920
 
921
921
  The clean rule:
@@ -16,7 +16,7 @@ source snapshot (join value from data / identifier / identity)
16
16
  → copy flat content.data into associated<ObjectType><ContentType>
17
17
  ```
18
18
 
19
- **Never** join a source snapshot directly to a non-snapshot collection by business keys such as `data.assetIp`.
19
+ **Never** join a source snapshot directly to a non-snapshot collection by business keys such as `data.sku`.
20
20
 
21
21
  ---
22
22
 
@@ -24,9 +24,9 @@ source snapshot (join value from data / identifier / identity)
24
24
 
25
25
  ### Cover / Chapter 0 — Vulnerability patch framing
26
26
 
27
- **Incorrect:** The vulnerability patch wires `vulnerabilities-snapshots` directly to `vulnerabilities-discovery` / `vulnerabilities-decisions` by `data.assetIp`.
27
+ **Incorrect:** The vulnerability patch wires `findings-snapshots` directly to `findings-discovery` / `findings-decisions` by `data.sku`.
28
28
 
29
- **Correct:** The patch enriches `vulnerabilities-snapshots` using snapshot-mediated resolution: match snapshots first (including same-object `vulnerabilities-snapshots`), then fetch discovery/decisions content linked to the matched snapshot `recordId`.
29
+ **Correct:** The patch enriches `findings-snapshots` using snapshot-mediated resolution: match snapshots first (including same-object `findings-snapshots`), then fetch discovery/decisions content linked to the matched snapshot `recordId`.
30
30
 
31
31
  ### Chapter 4 — Core association rules
32
32
 
@@ -38,10 +38,10 @@ source snapshot (join value from data / identifier / identity)
38
38
 
39
39
  Replace the direct-join walkthrough with:
40
40
 
41
- 1. Read source `vulnerabilities-snapshots.data.assetIp`.
42
- 2. Match target `vulnerabilities-snapshots` by snapshot paths such as `data.assetIp`, `concept.identifier`, or `identity`.
41
+ 1. Read source `findings-snapshots.data.sku`.
42
+ 2. Match target `findings-snapshots` by snapshot paths such as `data.sku`, `concept._extIdentifier`, or `identity`.
43
43
  3. Take matched target snapshot `recordId`.
44
- 4. Fetch `vulnerabilities-discovery` / `vulnerabilities-decisions` by a record-id link field such as `data.sourceRecordId`, `concept.identifier.recordId`, or root `recordId`.
44
+ 4. Fetch `findings-discovery` / `findings-decisions` by a record-id link field such as `data.sourceRecordId`, `concept._extIdentifier.recordId`, or root `recordId`.
45
45
  5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
46
46
 
47
47
  Payload rules for this pilot:
@@ -71,10 +71,10 @@ Payload rules for this pilot:
71
71
  ## Replacement example (vulnerability discovery/decisions)
72
72
 
73
73
  ```markdown
74
- 1. Read source `vulnerabilities-snapshots.data.assetIp`.
75
- 2. Match target `vulnerabilities-snapshots` by snapshot paths such as `data.assetIp`, `concept.identifier`, or `identity`.
74
+ 1. Read source `findings-snapshots.data.sku`.
75
+ 2. Match target `findings-snapshots` by snapshot paths such as `data.sku`, `concept._extIdentifier`, or `identity`.
76
76
  3. Take matched target snapshot `recordId`.
77
- 4. Fetch `vulnerabilities-discovery` / `vulnerabilities-decisions` by a record-id link field such as `data.sourceRecordId`, `concept.identifier.recordId`, or root `recordId`.
77
+ 4. Fetch `findings-discovery` / `findings-decisions` by a record-id link field such as `data.sourceRecordId`, `concept._extIdentifier.recordId`, or root `recordId`.
78
78
  5. Copy flat `content.data` into `associatedVulnerabilitiesDiscovery` / `associatedVulnerabilitiesDecisions`.
79
79
  ```
80
80
 
@@ -84,7 +84,7 @@ Payload rules for this pilot:
84
84
 
85
85
  | Aspect | Incorrect (PDF Chapter 5) | Correct |
86
86
  |--------|---------------------------|---------|
87
- | Join path | `vulnerabilities-snapshots.data.assetIp` → `vulnerabilities-discovery.data.assetIp` | `vulnerabilities-snapshots.data.assetIp` → `vulnerabilities-snapshots` (snapshot match) → `recordId` → `vulnerabilities-discovery` (by `concept.identifier.recordId`) |
87
+ | Join path | `findings-snapshots.data.sku` → `findings-discovery.data.sku` | `findings-snapshots.data.sku` → `findings-snapshots` (snapshot match) → `recordId` → `findings-discovery` (by `concept._extIdentifier.recordId`) |
88
88
  | Phase count | One-phase direct content join | Two-phase: snapshot match, then content fetch |
89
89
  | Same-object rule | Exception allowing direct snapshot→content | Same-object still matches snapshots first |
90
90
  | Bridge pattern | Implicit direct join | Explicit `match` + `content` in associator rules |
@@ -95,15 +95,15 @@ Payload rules for this pilot:
95
95
 
96
96
  ```json
97
97
  {
98
- "sourceJoinPaths": ["data.assetIp"],
98
+ "sourceJoinPaths": ["data.sku"],
99
99
  "match": {
100
- "collection": "vulnerabilities-snapshots",
101
- "joinPaths": ["data.assetIp", "concept.identifier.assetIp", "identity", "recordId"]
100
+ "collection": "findings-snapshots",
101
+ "joinPaths": ["data.sku", "concept._extIdentifier.sku", "identity", "recordId"]
102
102
  },
103
103
  "content": {
104
- "collection": "vulnerabilities-discovery",
104
+ "collection": "findings-discovery",
105
105
  "contentType": "discovery",
106
- "linkPaths": ["concept.identifier.recordId", "data.sourceRecordId", "recordId"]
106
+ "linkPaths": ["concept._extIdentifier.recordId", "data.sourceRecordId", "recordId"]
107
107
  },
108
108
  "associatedProperty": "associatedVulnerabilitiesDiscovery",
109
109
  "payload": { "format": "v2", "source": "target.data", "includeRecordId": false }
@@ -55,33 +55,29 @@ Every piece of business data lives in **exactly one** of these buckets (plus the
55
55
 
56
56
  | Bucket | Contents |
57
57
  |---|---|
58
- | **`data`** | Raw facts about the entity itself — IP, severity, session timeout, zone name |
59
- | **`analytics`** | Rollups and counts computed from other records — vulnerability totals, affected-asset counts |
60
- | **`enrichment`** | External, sourced values joined from feeds or prior systems — CVSS, EPSS, KEV, VPR |
61
- | **`insights`** | AI-generated judgments — exploitability, potential impact, classification, risk narratives |
58
+ | **`data`** | Native facts about the subject itself |
59
+ | **`analytics`** | Numeric rollups computed from other records |
60
+ | **`enrichment`** | External / prior-system values as received |
61
+ | **`insights`** | Judgments, scores, recommendations |
62
62
  | **`associatedData`** | Linked snapshot/data content copied from associated records' `data` payloads |
63
63
  | **`associatedDiscovery`** | Linked discovery content copied from associated records' `data` payloads |
64
64
  | **`associatedAnalysis`** | Linked analysis content copied from associated records' `data` payloads |
65
- | **`associatedSiblings`** | Self-association: other snapshot `data` payloads sharing the same join key (e.g. assets by shared `data.subnetCidr` in `data.subnetCidrs[]`); each item includes linked `recordId`. Default cap: 99 items. |
66
- | **`associatedChildren`** | Parent-to-child association (subnets only today): asset `data` payloads whose `data.subnetCidrs` contains the subnet `data.cidr`; each item includes linked `recordId`. Default cap: 99 items. |
65
+ | **`associatedSiblings`** | Self-association: other snapshot `data` payloads sharing the same join key; each item includes linked `recordId`. Default cap: 99 items. |
66
+ | **`associatedChildren`** | Parent-to-child association: child `data` payloads whose join key matches the parent; each item includes linked `recordId`. Default cap: 99 items. |
67
67
 
68
68
  Do not duplicate a value across buckets. When reshaping legacy records, promote `sourceFacts` values into the correct bucket rather than wrapping them.
69
69
 
70
- Associated properties are always arrays and are omitted when no linked data was resolved. Do not write empty arrays, and do not switch between object and array shape. Custom associated content uses the same root-level pattern: `associatedCompliance`, `associatedBusinessContext`, `associatedExposure`, and so on.
70
+ Associated properties are always arrays and are omitted when no linked data was resolved. Do not write empty arrays, and do not switch between object and array shape. Custom associated content uses the same root-level pattern: `associatedCompliance`, `associatedBusinessContext`, and so on.
71
71
 
72
- **Ranked top associations** use `associatedTop<ObjectType><ContentType>` (e.g. `associatedTopVulnerabilitiesAnalysis`). Candidates are sorted by the linked object type’s `importanceIndicator` (or rule `rankBy`) and capped (default 5). See [association-format.md](../memorix-pipeline/docs/association-format.md).
72
+ **Ranked top associations** use `associatedTop<ObjectType><ContentType>` (e.g. `associatedTopItemsAnalysis`). Candidates are sorted by the linked object type’s `importanceIndicator` (or rule `rankBy`) and capped (default 5). See [association-format.md](./association-format.md).
73
73
 
74
- **Enrichment associations** use `associated<ObjectType>Enrichment` (e.g. `associatedVulnerabilitiesGroupsEnrichment`). They copy the linked snapshot’s root `enrichment` bucket (external feed values: CVSS, EPSS, KEV, compositeScore) as a flat object array — not a separate `*-enrichment` content collection. See [association-format.md](../memorix-pipeline/docs/association-format.md).
74
+ **Enrichment associations** use `associated<ObjectType>Enrichment` (e.g. `associatedItemGroupsEnrichment`). They copy the linked snapshot’s root `enrichment` bucket as a flat object array — not a separate `*-enrichment` content collection. See [association-format.md](./association-format.md).
75
75
 
76
76
  Associated items contain the linked record's `data` payload (or `enrichment` / `concept` / root property for the specialized forms). Legacy associations may also include inline `recordId` and `contentType`; v2 associations store traceability in optional item-level `_system.association` metadata. Do not wrap associated items with redundant envelope keys; failure and diagnostic details belong in `<objectType>-failed` collections instead.
77
77
 
78
78
  **Associated property metadata:** Consumers should prefer Memorix associated-property metadata when available (`@x12i/memorix-associator`). Associated snapshot properties are exposed through managed descriptors plus discovery. Until metadata is available, or when reading older/custom records, fall back to the prefix rule: root-level fields on snapshots whose names start with `associated`. The well-known names `associatedData`, `associatedDiscovery`, and `associatedAnalysis` are conventions only — the complete set comes from metadata plus the `associated*` fallback rule.
79
79
 
80
- When a field can naturally have more than one value, processing contracts use array shape even if the current record has only one value. For assets, `data.assetIps`, `data.subnets[]`, and `data.subnetCidrs[]` are the normalized join fields. Each `data.subnets[]` entry is a structured subnet object `{ cidr, networkAddress, prefixLength, mask }` — one resolved subnet per asset IP, chosen by longest-prefix match. Multiple entries exist only when multiple asset IPs resolve to different canonical subnets. `data.subnetCidrs[]` is the derived flat join index used by association rules.
81
-
82
- For subnets, `data.cidr` is the canonical join key. Subnet records also carry structured `data.subnet` plus flattened `data.networkAddress`, `data.prefixLength`, and `data.mask`, all derived deterministically from `data.cidr`.
83
-
84
- Assets also carry `data.assetIpClassifications[]` when topology resolution runs. Each item records `{ ip, scope, confidence, reason, reasoning, matchedSubnetCidr?, matchedSubnetIp? }`, where `scope` is `inside`, `outside`, or `unknown`. Convenience arrays `data.insideAssetIps`, `data.outsideAssetIps`, and `data.unknownAssetIps` may be populated from those classifications. `inside` means the IP either matched canonical topology with high confidence, or is RFC1918 private space without a matching topology CIDR with medium confidence. `outside` means public/non-RFC1918 with no topology match. `unknown` covers invalid, loopback, and link-local addresses.
80
+ When a field can naturally have more than one value, processing contracts use array shape even if the current record has only one value. Join-key recipes (which paths and shapes) are defined on **entity descriptors** for each Catalox object type — not in this format package.
85
81
 
86
82
  ---
87
83
 
@@ -89,49 +85,45 @@ Assets also carry `data.assetIpClassifications[]` when topology resolution runs.
89
85
 
90
86
  ```ts
91
87
  type MemorixConcept = {
92
- identifier: {
88
+ _extIdentifier: {
93
89
  kind: string;
94
90
  /** Primary value — property name matches `kind`. */
95
91
  [kind: string]: string;
96
92
  /**
97
93
  * Deterministic join key: `kind` first, then the kind-named value,
98
94
  * then any other scalar fields in sorted key order, joined by `:`.
99
- * See `.operational/fr/2026-07-09-memorix-format-identifier-key.md`.
100
95
  */
101
- identifierKey: string;
96
+ key: string;
102
97
  };
103
98
  title: string;
104
99
  };
105
100
  ```
106
101
 
107
- `identifier` is a discriminated object: `kind` names the id type, and a same-named property holds the value. `identifierKey` is required on every identifier (root and nested concepts).
102
+ `_extIdentifier` is a discriminated object: `kind` names the id type, and a same-named property holds the value. `key` is required. How `kind` / title are derived for an object type is defined on the **entity descriptor**, not here.
108
103
 
109
104
  ```json
110
105
  "concept": {
111
- "identifier": {
112
- "kind": "ipAddress",
113
- "ipAddress": "10.150.68.31",
114
- "identifierKey": "ipAddress:10.150.68.31"
106
+ "_extIdentifier": {
107
+ "kind": "sku",
108
+ "sku": "SKU-100",
109
+ "key": "sku:SKU-100"
115
110
  },
116
- "title": "BGAK105-002"
111
+ "title": "Blue widget"
117
112
  }
118
113
  ```
119
114
 
120
- `concept.name` is not used. `title` is the human-readable display name; it may differ from the identifier value (e.g. hostname vs IP).
115
+ `concept.name` is not used. `concept.identifier` / `identifierKey` are legacy — migrate to `_extIdentifier` / `key`. `title` is the human-readable display name; it may differ from the identifier value.
121
116
 
122
- ### Identifier `kind` by object type
117
+ ### Identifier `kind` (illustrative — not format law)
123
118
 
124
- | Object type | `kind` | Example identifier |
119
+ | Object type (abstract) | `kind` | Example |
125
120
  |---|---|---|
126
- | Assets | `ipAddress` | `{"kind":"ipAddress","ipAddress":"10.150.68.31","identifierKey":"ipAddress:10.150.68.31"}` |
127
- | Vulnerability groups | `pluginId` (preferred) or `cveId` | `{"kind":"pluginId","pluginId":"142594","identifierKey":"pluginId:142594"}` |
128
- | Vulnerability instances (host findings) | `hostVulnerability` | `{"kind":"hostVulnerability","hostVulnerability":"10.100.18.64::pluginId:160208","assetIp":"10.100.18.64","vulnerabilityGroupIdentifierKey":"pluginId:160208","identifierKey":"hostVulnerability:10.100.18.64::pluginId:160208:10.100.18.64:pluginId:160208"}` |
129
- | Subnets | `cidr` (canonical) or `subnetIp` (legacy, quarantined) | `{"kind":"cidr","cidr":"1.1.1.5/32","identifierKey":"cidr:1.1.1.5/32"}` |
130
- | Topology zones | `zoneName` | `{"kind":"zoneName","zoneName":"LAN","identifierKey":"zoneName:LAN"}` |
131
- | Topology VRs | `vrName` | `{"kind":"vrName","vrName":"VR-UPB-MED","identifierKey":"vrName:VR-UPB-MED"}` |
132
- | Users | `sessionKey` | `{"kind":"sessionKey","sessionKey":"med\\cap\|10.39.44.205\|UIA","identifierKey":"sessionKey:med\\cap\|10.39.44.205\|UIA"}` |
121
+ | Items | `sku` | `{"kind":"sku","sku":"SKU-100","key":"sku:SKU-100"}` |
122
+ | Groups | `groupCode` | `{"kind":"groupCode","groupCode":"G-42","key":"groupCode:G-42"}` |
123
+ | Locations | `locationCode` | `{"kind":"locationCode","locationCode":"WH-A","key":"locationCode:WH-A"}` |
124
+ | Documents | `docId` | `{"kind":"docId","docId":"DOC-9","key":"docId:DOC-9"}` |
133
125
 
134
- Vulnerability groups are **plugin-scoped**: prefer `pluginId` whenever present. Use `cveId` only when there is no plugin id — the same CVE can appear on multiple plugins, so CVE is not a unique group key. Keep `data.cveId` for display/filter. Vulnerability **instances** also denormalize `vulnerabilityGroupIdentifierKey` at the record root for queries.
126
+ Domain packs (Catalox) may use different `kind` values; those recipes live on descriptors.
135
127
 
136
128
  ---
137
129
 
@@ -143,6 +135,7 @@ System metadata — provenance, processing history, graph-run stamps — never b
143
135
  type MemorixLifecycleEvent = { process: string; processedAt: string };
144
136
 
145
137
  type MemorixSystemBlock = {
138
+ state?: "active" | "archived" | "superseded";
146
139
  lifecycle?: MemorixLifecycleEvent[];
147
140
  _graphRuns?: Record<string, GraphRunEntry>;
148
141
  _jobTypeRuns?: Record<string, GraphRunEntry>;
@@ -164,8 +157,8 @@ Root-level `lifecycle` is gone. Processing events live at `_system.lifecycle`:
164
157
  - Each entry is `{ process, processedAt }`.
165
158
  - When `createdAt`, `modifiedAt`, and `capturedAt` are identical (the common snapshot case), emit a single `"enriched"` event rather than three duplicate timestamps.
166
159
  - Distinct events (e.g. a future re-enrichment) append as additional array entries.
167
- - `status` is not stored; it was uniformly `"active"` across all records.
168
- - Omit `lifecycle` entirely when no timestamp is known (e.g. Users records with no source dates) — do not emit an empty array.
160
+ - Omit `lifecycle` entirely when no timestamp is known — do not emit an empty array.
161
+ - `_system.state` is the present-tense lifecycle state enum (`active` \| `archived` \| `superseded`).
169
162
 
170
163
  `_system.provenance.enrichedAt` (when present) is separate provenance metadata about a prior enrichment step, not a substitute for `lifecycle`.
171
164
 
@@ -179,102 +172,59 @@ Dot-path references: `_system._graphRuns.<graphId>`, `_system._jobTypeRuns.<jobT
179
172
 
180
173
  ## Complete example
181
174
 
182
- Vulnerability group record showing all envelope fields and every bucket:
175
+ Abstract item snapshot showing envelope fields and open buckets:
183
176
 
184
177
  ```json
185
178
  {
186
- "_id": "6914a132880706529ece3da1",
187
- "recordId": "69d216ed358778e93215787e",
179
+ "recordId": "rec-item-1",
188
180
  "concept": {
189
- "identifier": {
190
- "kind": "pluginId",
191
- "pluginId": "142594",
192
- "identifierKey": "pluginId:142594"
181
+ "_extIdentifier": {
182
+ "kind": "sku",
183
+ "sku": "SKU-100",
184
+ "key": "sku:SKU-100"
193
185
  },
194
- "title": "Oracle WebLogic Server RCE (CVE-2020-14882)"
186
+ "title": "Blue widget"
195
187
  },
196
188
  "data": {
197
- "name": "Oracle WebLogic Server RCE (CVE-2020-14882)",
198
- "vulnerability_family": "Web Servers",
199
- "vulnerability_type": "cve",
200
- "cveId": "CVE-2020-14882",
201
- "severity": "critical",
202
- "pluginId": 142594
189
+ "sku": "SKU-100",
190
+ "color": "blue",
191
+ "family": "widgets"
203
192
  },
204
193
  "associatedData": [
205
- { "assetIp": "10.100.24.31", "hostname": "BGAK105-002" },
206
- { "assetIp": "10.100.24.41", "hostname": "BGAK105-003" }
194
+ { "locationCode": "WH-A", "qty": 12 },
195
+ { "locationCode": "WH-B", "qty": 3 }
207
196
  ],
208
197
  "analytics": {
209
- "affectedAssets": { "total": 2 }
198
+ "linkedLocations": { "total": 2 }
210
199
  },
211
200
  "enrichment": {
212
- "cvss": {
213
- "baseScore": 9.8,
214
- "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
215
- },
216
- "epss": { "epss": 0.94454, "percentile": 0.99992 },
217
- "kev": { "isKnownExploited": true },
218
- "compositeScore": 0.974,
219
- "riskLevel": "CRITICAL",
220
- "vprScore": "9.2"
201
+ "catalogScore": 0.91,
202
+ "sourceSystem": "erp-feed"
221
203
  },
222
204
  "insights": {
223
- "exploitability": { "level": "HIGH", "confidence": 0.92 },
224
- "potentialImpact": { "level": "CRITICAL", "confidence": 0.88 }
205
+ "priority": { "level": "high", "confidence": 0.9 }
225
206
  },
226
207
  "_system": {
208
+ "state": "active",
227
209
  "provenance": {
228
- "sourceId": "6914a132880706529ece3da1",
229
- "snapshotRecordId": "69d216ed358778e93215787e",
230
210
  "source": {
231
- "database": "poc-data-mapping",
232
- "collection": "vulnerabilities-groups-rawdata",
233
- "server": "default"
211
+ "database": "example-db",
212
+ "collection": "items-rawdata"
234
213
  }
235
214
  },
236
215
  "lifecycle": [
237
216
  { "process": "enriched", "processedAt": "2026-04-05T08:01:15.511Z" }
238
- ],
239
- "_graphRuns": {
240
- "graph-t92669z": {
241
- "status": "done",
242
- "startedAt": "2026-07-01T23:41:23.233Z",
243
- "completedAt": "2026-07-01T23:42:05.604Z",
244
- "lastJobRunId": "f7af7f7a-0c5c-4825-9282-d39bea836682",
245
- "attempts": 1,
246
- "result": {
247
- "target": "entity",
248
- "entityName": "vulnerabilities-groups",
249
- "contentType": "inferences",
250
- "recordId": "75de9244-8d44-43b8-8698-7e22621b1087"
251
- }
252
- }
253
- }
217
+ ]
254
218
  }
255
219
  }
256
220
  ```
257
221
 
258
- Not every record populates every bucket. Assets typically have `data`, `analytics`, and optionally `insights` and `associatedData`; only vulnerability groups carry `enrichment` today.
222
+ Not every record populates every bucket. Non-snapshot content types usually keep substance in `.data` only (SCOPE).
259
223
 
260
224
  ---
261
225
 
262
226
  ## Migration notes
263
227
 
264
- - Reshape scripts (`scripts/memorix-neo-reshape/transform-20-to-clean.mjs`) convert 2.0 envelopes to this format.
265
- - Fixed POC reference data lives in `temp/data-and-format-fixes/` with per-collection change notes in `CHANGES.md`.
266
- - Type definitions: `memorix-descriptors/src/record/envelope.ts`.
267
-
268
- ### Subnets / topology subnets merge (2026-07-05)
269
-
270
- `topology-subnets` was merged into `subnets`. Canonical `subnets` records are topology
271
- CIDR segments (`concept.identifier.kind: "cidr"`), spread across content types
272
- `snapshots` / `core` / `inferences` / `scoped`. A subset of the old, pre-merge `subnets`
273
- records (subnetIp-only, no CIDR) had zero overlap with any canonical CIDR and were
274
- quarantined rather than dropped — they live in the `legacy` content type
275
- (`subnets-legacy`), keyed by `concept.identifier.kind: "subnetIp"`, with an explicit
276
- `_system.quarantine` block (`legacy: true`, `status`, `reason`) marking them as not
277
- eligible for `assets -> subnets` joins. `assets.data.subnetCidr` (precomputed via CIDR
278
- containment against canonical `subnets-snapshots`) is the join key used for that
279
- relation — see `scripts/memorix-subnets-merge/` for the migration and backfill scripts,
280
- and `temp/part-b-implementation-design-2026-07-05T06-20-00Z.md` for the full design.
228
+ - Prefer **`npx memorix-format migrate`** for transforms (dry-run → `--out` → validate). Live Mongo apply is a separate ops step.
229
+ - Historical reshape scripts under `scripts/` are not the recommended path.
230
+ - Thin envelope helpers may still be re-exported from `@x12i/memorix-descriptors`; **format packages are the SOT**.
@@ -73,7 +73,7 @@ type WorkflowTask = {
73
73
 
74
74
  Catalog id: `memorix-workflow-field-schemas`
75
75
 
76
- One item per object type (natural id e.g. `assets-workflow-fields`):
76
+ One item per object type (natural id e.g. `items-workflow-fields`):
77
77
 
78
78
  ```ts
79
79
  type WorkflowFieldSchemaItem = {
@@ -1,25 +1,25 @@
1
1
  {
2
- "recordId": "69d216ed358778e93215787e",
2
+ "recordId": "rec-item-1",
3
3
  "concept": {
4
4
  "_extIdentifier": {
5
- "kind": "pluginId",
6
- "pluginId": "142594",
7
- "key": "pluginId:142594"
5
+ "kind": "sku",
6
+ "sku": "SKU-100",
7
+ "key": "sku:SKU-100"
8
8
  },
9
- "title": "Oracle WebLogic Server RCE (CVE-2020-14882)"
9
+ "title": "Blue widget"
10
10
  },
11
11
  "data": {
12
- "cveId": "CVE-2020-14882",
13
- "severity": "critical"
12
+ "sku": "SKU-100",
13
+ "color": "blue"
14
14
  },
15
15
  "associatedData": [
16
- { "assetIp": "10.100.24.31", "hostname": "BGAK105-002" }
16
+ { "locationCode": "WH-A", "qty": 12 }
17
17
  ],
18
18
  "analytics": {
19
- "affectedAssets": { "total": 1 }
19
+ "linkedLocations": { "total": 1 }
20
20
  },
21
21
  "insights": {
22
- "exploitability": { "level": "HIGH", "confidence": 0.92 }
22
+ "priority": { "level": "high", "confidence": 0.9 }
23
23
  },
24
24
  "_system": {
25
25
  "state": "active",
@@ -72,6 +72,7 @@
72
72
  "allContentTypesCoveredInContentTypesJson": true,
73
73
  "platformOpenBucketsPresentInOpenBucketsJson": ["analytics", "enrichment", "insights"],
74
74
  "associatedNamesFollowPrefixConvention": true,
75
- "noObjectTypesInFormatLawJson": true
75
+ "noObjectTypesInFormatLawJson": true,
76
+ "conceptRecipesOnEntityDescriptors": true
76
77
  }
77
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/memorix-format",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Memorix product format on @x12i/memorix-base-format: open buckets, associations, SCOPE, migrate CLI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",