@x12i/memorix-associator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +90 -0
  2. package/dist/associated-properties/index.d.ts +16 -0
  3. package/dist/associated-properties/index.d.ts.map +1 -0
  4. package/dist/associated-properties/index.js +118 -0
  5. package/dist/associated-properties/index.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +88 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/engine.d.ts +12 -0
  11. package/dist/engine.d.ts.map +1 -0
  12. package/dist/engine.js +115 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/index.d.ts +12 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +13 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/planning/build-plan.d.ts +10 -0
  19. package/dist/planning/build-plan.d.ts.map +1 -0
  20. package/dist/planning/build-plan.js +319 -0
  21. package/dist/planning/build-plan.js.map +1 -0
  22. package/dist/planning/fingerprint.d.ts +19 -0
  23. package/dist/planning/fingerprint.d.ts.map +1 -0
  24. package/dist/planning/fingerprint.js +47 -0
  25. package/dist/planning/fingerprint.js.map +1 -0
  26. package/dist/request/schema.d.ts +213 -0
  27. package/dist/request/schema.d.ts.map +1 -0
  28. package/dist/request/schema.js +2 -0
  29. package/dist/request/schema.js.map +1 -0
  30. package/dist/request/validate.d.ts +6 -0
  31. package/dist/request/validate.d.ts.map +1 -0
  32. package/dist/request/validate.js +64 -0
  33. package/dist/request/validate.js.map +1 -0
  34. package/dist/store/mongo.d.ts +4 -0
  35. package/dist/store/mongo.d.ts.map +1 -0
  36. package/dist/store/mongo.js +26 -0
  37. package/dist/store/mongo.js.map +1 -0
  38. package/dist/store/types.d.ts +30 -0
  39. package/dist/store/types.d.ts.map +1 -0
  40. package/dist/store/types.js +64 -0
  41. package/dist/store/types.js.map +1 -0
  42. package/dist/verification/associated-shape.d.ts +7 -0
  43. package/dist/verification/associated-shape.d.ts.map +1 -0
  44. package/dist/verification/associated-shape.js +52 -0
  45. package/dist/verification/associated-shape.js.map +1 -0
  46. package/dist/writing/failures.d.ts +13 -0
  47. package/dist/writing/failures.d.ts.map +1 -0
  48. package/dist/writing/failures.js +18 -0
  49. package/dist/writing/failures.js.map +1 -0
  50. package/dist/writing/smart-merge.d.ts +19 -0
  51. package/dist/writing/smart-merge.d.ts.map +1 -0
  52. package/dist/writing/smart-merge.js +77 -0
  53. package/dist/writing/smart-merge.js.map +1 -0
  54. package/docs/running-associations.md +340 -0
  55. package/examples/minimal-association-request.json +46 -0
  56. package/package.json +57 -0
@@ -0,0 +1,340 @@
1
+ # Running associations between object types
2
+
3
+ This guide explains how to **plan**, **apply**, and **re-run** cross-record associations using `@x12i/memorix-associator`.
4
+
5
+ An association copies linked records' `data` payloads onto source snapshot records under root-level `associated*` fields (for example `associatedData`, `associatedInferred`, or custom names like `associatedAssets`). The associator never writes back into linked collections.
6
+
7
+ ---
8
+
9
+ ## What you need before you start
10
+
11
+ | Requirement | Notes |
12
+ |-------------|-------|
13
+ | MongoDB access | Set `MONGO_URI` (or the env var named in the request's `connection.mongoUriEnv`). |
14
+ | Source collection | Snapshot records that will receive associated payloads. |
15
+ | Linked collections | Records to join against; each must have a `data` object. |
16
+ | Association request | A JSON file describing source, rules, and safety settings (see below). |
17
+ | Built package | From this directory: `npm run build`, then use `npx memorix-associator` or the API. |
18
+
19
+ ---
20
+
21
+ ## Two ways to define an association
22
+
23
+ ### 1. Metadata-managed associations (preferred)
24
+
25
+ When an entity descriptor already declares `associatedProperties`, the association is **managed**: metadata names the property, linked object type, linked content type, and usually a `refreshRuleKey`.
26
+
27
+ Example from entity descriptors:
28
+
29
+ ```json
30
+ {
31
+ "name": "associatedData",
32
+ "linkedObjectType": "subnets",
33
+ "linkedContentType": "snapshots",
34
+ "refreshRuleKey": "assets.subnets.snapshots.by-cidr",
35
+ "managed": true
36
+ }
37
+ ```
38
+
39
+ **How to run today**
40
+
41
+ 1. Find the managed property on the **source** entity descriptor (Schema UI → entity → Associated properties, or `associatedProperties` in the descriptor JSON).
42
+ 2. Use a request file whose `rules[].ruleKey` matches `refreshRuleKey` and whose join paths match the operational definition.
43
+ 3. Run the standard plan → review → apply workflow (below).
44
+
45
+ The repo maintains a coverage registry at `.operational/metadata/associated-properties-registry.json` listing known managed associations, join paths, and last apply timestamps.
46
+
47
+ **Known gap:** metadata describes *what* is associated and *which rule refreshes it*, but the associator still needs a full **request JSON** with join paths, collections, and resolver steps. There is not yet a `memorix-associator run --refresh-rule-key …` command that builds the request from metadata alone. Until that exists, keep request files alongside operational presets (or generate them from the registry).
48
+
49
+ **Do not auto-refresh discovered-only properties.** Fields that exist only because records happen to have an `associated*` key (no managed descriptor entry) can be read by consumers, but you should not run a managed refresh for them until a rule and descriptor entry exist.
50
+
51
+ ### 2. Ad-hoc associations
52
+
53
+ For experiments, one-off joins, or associations not yet promoted to metadata, write a request file directly. The same plan → apply → verify workflow applies.
54
+
55
+ Use ad-hoc when:
56
+
57
+ - You are prototyping a new link between two object types.
58
+ - Metadata has not been updated yet.
59
+ - You need a narrow `source.filter` (subset of records only).
60
+
61
+ After a successful ad-hoc run, promote the association by adding `associatedProperties` to the entity descriptor and recording the `refreshRuleKey` in operational metadata.
62
+
63
+ ---
64
+
65
+ ## The workflow: plan → review → apply → verify
66
+
67
+ Always run in this order. **Apply is gated:** when `safety.requireReviewedReportForApply` is `true` (recommended), apply refuses to run unless the plan fingerprint matches a reviewed dry-run report.
68
+
69
+ ```
70
+ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
71
+ │ plan │ ──► │ review │ ──► │ apply │ ──► │ verify │
72
+ │dry-run │ │ report │ │ writes │ │ shapes │
73
+ └─────────┘ └─────────┘ └─────────┘ └─────────┘
74
+ ```
75
+
76
+ ### 1. Plan (dry-run)
77
+
78
+ Scans source and linked collections, computes joins, and produces a report **without writing** to source snapshots (except failure collections are only written on apply).
79
+
80
+ ```bash
81
+ export MONGO_URI="mongodb://…"
82
+
83
+ memorix-associator plan \
84
+ --request examples/minimal-association-request.json \
85
+ --output temp/my-association-plan.json
86
+ ```
87
+
88
+ **What to check in the report**
89
+
90
+ | Field | Meaning |
91
+ |-------|---------|
92
+ | `planFingerprint` | Stable hash of pending updates; required for apply. |
93
+ | `rules[]` | Per-rule scan counts, skipped rules, missing join stats. |
94
+ | `sourceCollections[].sourceRecordsWouldUpdate` | How many source records would change. |
95
+ | `sourceCollections[].associatedItemsWouldAppend` | New `data` payloads that would be appended. |
96
+ | `failureCountsByReason` | Join/shape failures grouped by reason. |
97
+ | `failureSamples` | Example failure records (when configured). |
98
+
99
+ If counts look wrong, fix join paths, resolvers, or source data before applying.
100
+
101
+ ### 2. Review
102
+
103
+ Open the plan report and confirm:
104
+
105
+ - Rule keys and linked collections are correct.
106
+ - Update counts are expected for the data volume.
107
+ - Failure reasons are acceptable (optional rules may legitimately have gaps).
108
+ - `planFingerprint` is noted (apply will require it).
109
+
110
+ ### 3. Apply
111
+
112
+ Writes associated arrays on source records using **append-unique** merge. Optionally backs up the source collection when `safety.backupBeforeApply` is `true`.
113
+
114
+ ```bash
115
+ memorix-associator apply \
116
+ --request examples/minimal-association-request.json \
117
+ --reviewed-report temp/my-association-plan.json
118
+ ```
119
+
120
+ Apply fails if the current plan fingerprint does not match the reviewed report. That means underlying data or the request changed since the dry-run — **re-plan and review again**.
121
+
122
+ ### 4. Verify (optional but recommended)
123
+
124
+ Checks that associated fields on source records are well-formed arrays of objects.
125
+
126
+ ```bash
127
+ memorix-associator verify --request examples/minimal-association-request.json
128
+ ```
129
+
130
+ You can also set `safety.verifyAfterPlan` or `safety.verifyAfterApply` in the request to run verification automatically.
131
+
132
+ ---
133
+
134
+ ## Re-running associations (important)
135
+
136
+ Re-running is **safe and expected**. Association apply is **idempotent**: running the same request again after a successful apply should produce **zero** pending updates.
137
+
138
+ ### What re-run does
139
+
140
+ 1. **Normalizes** existing associated values to arrays and deduplicates by canonical deep equality.
141
+ 2. **Appends** linked `data` payloads that are not already present.
142
+ 3. **Omits** associated properties that would be empty.
143
+ 4. **Does not remove** previously appended items when a link disappears or linked `data` changes — merge is append-only. A changed linked payload may append as a *new* item if its canonical form differs; stale copies are not automatically pruned.
144
+
145
+ ### When to re-run
146
+
147
+ | Trigger | Action |
148
+ |---------|--------|
149
+ | New source records ingested | Plan → apply same request. |
150
+ | Linked records added or updated | Plan → apply; only new/changed payloads append. |
151
+ | Join fields normalized (e.g. resolver wrote `data.subnetCidrs`) | Re-run after normalization/resolver steps. |
152
+ | Previous apply failed partway | Fix data or request; re-plan from scratch. |
153
+ | Routine refresh | Re-run managed requests on a schedule or after linked collection updates. |
154
+
155
+ ### How to confirm convergence
156
+
157
+ After apply, run **plan** again with the same request:
158
+
159
+ ```bash
160
+ memorix-associator plan \
161
+ --request examples/minimal-association-request.json \
162
+ --output temp/my-association-recheck.json
163
+ ```
164
+
165
+ A converged run shows:
166
+
167
+ - `associatedItemsWouldAppend`: **0**
168
+ - `sourceRecordsWouldUpdate`: **0**
169
+ - Empty or no pending updates in the plan
170
+
171
+ That zero-append dry-run is the main signal that the process is deterministic and caught up.
172
+
173
+ ### Re-run checklist
174
+
175
+ 1. Use the **same** request file (or an updated version you have reviewed).
176
+ 2. Always **plan first** — never assume a previous fingerprint still applies.
177
+ 3. Compare the new fingerprint to the last reviewed report; if it changed, review the new plan before apply.
178
+ 4. For large collections, use `safety.limit` during investigation, then remove the limit for the full run.
179
+
180
+ ---
181
+
182
+ ## Request file structure (generic)
183
+
184
+ Every run is driven by one JSON request. Minimal shape:
185
+
186
+ ```json
187
+ {
188
+ "processId": "source-object.linked-object.by-join",
189
+ "mode": "dry-run",
190
+ "connection": {
191
+ "mongoUriEnv": "MONGO_URI"
192
+ },
193
+ "safety": {
194
+ "requireReviewedReportForApply": true,
195
+ "backupBeforeApply": true,
196
+ "verifyAfterApply": true
197
+ },
198
+ "source": {
199
+ "db": "entities",
200
+ "collection": "source-object-snapshots",
201
+ "objectType": "source-object",
202
+ "contentType": "snapshots"
203
+ },
204
+ "rules": [
205
+ {
206
+ "type": "association",
207
+ "ruleKey": "source-object.linked-object.snapshots.by-field",
208
+ "mandatory": false,
209
+ "sourceJoinPaths": ["data.joinField"],
210
+ "target": {
211
+ "db": "entities",
212
+ "collection": "linked-object-snapshots",
213
+ "objectType": "linked-object",
214
+ "contentType": "snapshots",
215
+ "joinPaths": ["data.matchField"]
216
+ },
217
+ "associatedProperty": "associatedData",
218
+ "payload": { "source": "target.data" }
219
+ }
220
+ ],
221
+ "failurePolicy": {
222
+ "writeFailures": true,
223
+ "includeSamplesInReport": 10
224
+ },
225
+ "writePolicy": {
226
+ "associatedArrayMode": "append-unique",
227
+ "omitEmptyAssociatedArrays": true,
228
+ "canonicalDeduplication": true
229
+ }
230
+ }
231
+ ```
232
+
233
+ ### Rule fields
234
+
235
+ | Field | Purpose |
236
+ |-------|---------|
237
+ | `ruleKey` | Stable id; should match `refreshRuleKey` in metadata when managed. |
238
+ | `mandatory` | If `true`, missing links write a failure record to `<objectType>-failed`. |
239
+ | `sourceJoinPaths` | Paths on the source record used as join keys (arrays fan out). |
240
+ | `target.joinPaths` | Paths on linked records to match against. |
241
+ | `target.bridge` | Optional bridge collection when join keys differ (e.g. name → record id). |
242
+ | `associatedProperty` | Root field on the source snapshot; must start with `associated`. |
243
+ | `payload.source` | Always `"target.data"` — only the linked record's `data` is copied. |
244
+
245
+ ### Optional: normalization and resolvers
246
+
247
+ Run **before** association rules when join keys need preparation:
248
+
249
+ - `normalization` — split comma-separated values into array join fields.
250
+ - `resolvers` — domain logic (e.g. CIDR containment) via `@x12i/memorix-resolvers`.
251
+
252
+ See [memorix-resolvers README](../../memorix-resolvers/README.md) for resolver types.
253
+
254
+ ### Optional: scope a run
255
+
256
+ ```json
257
+ "source": {
258
+ "filter": { "recordId": "specific-record-id" },
259
+ ...
260
+ },
261
+ "safety": {
262
+ "limit": 100
263
+ }
264
+ ```
265
+
266
+ ---
267
+
268
+ ## What gets written
269
+
270
+ | Destination | Content |
271
+ |-------------|---------|
272
+ | Source snapshot `associated*` fields | Array of linked `data` objects (append-unique). |
273
+ | `<source.objectType>-failed` | Technical failure records when `failurePolicy.writeFailures` is `true`. |
274
+ | Backup collection | Created when `safety.backupBeforeApply` is `true` (apply only). |
275
+
276
+ Associated arrays never contain join metadata, pointers, or wrapper objects — only the linked record's `data` payload.
277
+
278
+ ---
279
+
280
+ ## Programmatic API
281
+
282
+ Same workflow as the CLI:
283
+
284
+ ```typescript
285
+ import { MongoClient } from "mongodb";
286
+ import {
287
+ planAssociations,
288
+ applyAssociations,
289
+ verifyAssociations,
290
+ createMongoRecordStore,
291
+ } from "@x12i/memorix-associator";
292
+
293
+ const client = new MongoClient(process.env.MONGO_URI!);
294
+ await client.connect();
295
+ const store = createMongoRecordStore(client);
296
+
297
+ const plan = await planAssociations(request, { store });
298
+ // Review plan.report.planFingerprint
299
+
300
+ await applyAssociations(
301
+ {
302
+ ...request,
303
+ mode: "apply",
304
+ safety: {
305
+ ...request.safety,
306
+ expectedPlanFingerprint: plan.planFingerprint,
307
+ },
308
+ },
309
+ { store }
310
+ );
311
+
312
+ await verifyAssociations(request, { store });
313
+ await client.close();
314
+ ```
315
+
316
+ Explorer API also exposes `POST /associations/plan`, `/apply`, and `/verify` when running `memorix-explorer-api`.
317
+
318
+ ---
319
+
320
+ ## Failure reasons (quick reference)
321
+
322
+ | Reason | Typical cause |
323
+ |--------|----------------|
324
+ | `JOIN_FIELD_MISSING_ON_SOURCE` | Source record lacks a join path value. |
325
+ | `JOIN_FIELD_MISSING_ON_TARGET` | Linked record lacks a join path value. |
326
+ | `MANDATORY_LINKED_CONTEXT_MISSING` | Mandatory rule found no match. |
327
+ | `INVALID_DATA_SHAPE` | Linked record has no usable `data` object. |
328
+ | `BRIDGE_COLLECTION_MISSING` | Bridge collection configured but not found. |
329
+ | `TARGET_COLLECTION_MISSING` | Rule skipped — linked collection does not exist. |
330
+
331
+ Inspect `failureSamples` in the plan report and the `<objectType>-failed` collection for details.
332
+
333
+ ---
334
+
335
+ ## Related documentation
336
+
337
+ - [Package README](../README.md) — API surface and associated-property metadata.
338
+ - [Memorix format — associated buckets](../../docs/memorix-format.md)
339
+ - [Enrichment processes design note](../../memorix-schema-api/src/operational/memorix-enrichment-processes.md)
340
+ - [Associated properties registry](../../.operational/metadata/associated-properties-registry.json) — live managed association catalog.
@@ -0,0 +1,46 @@
1
+ {
2
+ "processId": "source-object.linked-object.context",
3
+ "description": "Generic template — replace db, collection, object types, join paths, and associatedProperty for your association.",
4
+ "mode": "dry-run",
5
+ "connection": {
6
+ "mongoUriEnv": "MONGO_URI"
7
+ },
8
+ "safety": {
9
+ "requireReviewedReportForApply": true,
10
+ "backupBeforeApply": true,
11
+ "verifyAfterPlan": false,
12
+ "verifyAfterApply": true
13
+ },
14
+ "source": {
15
+ "db": "entities",
16
+ "collection": "source-object-snapshots",
17
+ "objectType": "source-object",
18
+ "contentType": "snapshots"
19
+ },
20
+ "rules": [
21
+ {
22
+ "type": "association",
23
+ "ruleKey": "source-object.linked-object.snapshots.by-field",
24
+ "mandatory": false,
25
+ "sourceJoinPaths": ["data.joinField"],
26
+ "target": {
27
+ "db": "entities",
28
+ "collection": "linked-object-snapshots",
29
+ "objectType": "linked-object",
30
+ "contentType": "snapshots",
31
+ "joinPaths": ["data.matchField"]
32
+ },
33
+ "associatedProperty": "associatedData",
34
+ "payload": { "source": "target.data" }
35
+ }
36
+ ],
37
+ "failurePolicy": {
38
+ "writeFailures": true,
39
+ "includeSamplesInReport": 10
40
+ },
41
+ "writePolicy": {
42
+ "associatedArrayMode": "append-unique",
43
+ "omitEmptyAssociatedArrays": true,
44
+ "canonicalDeduplication": true
45
+ }
46
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@x12i/memorix-associator",
3
+ "version": "1.0.0",
4
+ "description": "Memorix cross-record association planning, apply, verification, and associated-property metadata",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "memorix-associator": "./dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "docs",
21
+ "examples"
22
+ ],
23
+ "scripts": {
24
+ "prebuild": "rm -rf dist",
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "prepack": "npm run build"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "@x12i/memorix-resolvers": "^1.1.0",
35
+ "mongodb": "^6.21.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.14.0",
39
+ "typescript": "^5.6.0",
40
+ "vitest": "^2.1.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+ssh://git@github.com/x12i/memorix-mono-repo.git",
48
+ "directory": "memorix-associator"
49
+ },
50
+ "keywords": [
51
+ "memorix",
52
+ "associator",
53
+ "associated",
54
+ "enrichment"
55
+ ],
56
+ "license": "exellix-license"
57
+ }