@x12i/memorix-format 2.2.3 → 2.3.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/CHANGELOG.md +26 -0
- package/README.md +36 -86
- package/dist/base-reexport.d.ts +8 -0
- package/dist/base-reexport.d.ts.map +1 -0
- package/dist/base-reexport.js +8 -0
- package/dist/base-reexport.js.map +1 -0
- package/dist/cli.js +179 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/v2/index.d.ts +7 -0
- package/dist/v2/index.d.ts.map +1 -0
- package/dist/v2/index.js +6 -0
- package/dist/v2/index.js.map +1 -0
- package/dist/v2/migrate-present.d.ts +47 -0
- package/dist/v2/migrate-present.d.ts.map +1 -0
- package/dist/v2/migrate-present.js +225 -0
- package/dist/v2/migrate-present.js.map +1 -0
- package/dist/v2/profiles.d.ts +9 -0
- package/dist/v2/profiles.d.ts.map +1 -0
- package/dist/v2/profiles.js +63 -0
- package/dist/v2/profiles.js.map +1 -0
- package/dist/v2/revision.d.ts +24 -0
- package/dist/v2/revision.d.ts.map +1 -0
- package/dist/v2/revision.js +44 -0
- package/dist/v2/revision.js.map +1 -0
- package/dist/v2/types.d.ts +94 -0
- package/dist/v2/types.d.ts.map +1 -0
- package/dist/v2/types.js +3 -0
- package/dist/v2/types.js.map +1 -0
- package/dist/v2/validate.d.ts +8 -0
- package/dist/v2/validate.d.ts.map +1 -0
- package/dist/v2/validate.js +225 -0
- package/dist/v2/validate.js.map +1 -0
- package/docs/association-format.md +3 -0
- package/docs/memorix-format.md +24 -186
- package/docs/memorix-record-v2.md +101 -0
- package/docs/memorix-workflow-format.md +13 -1
- package/docs/migrate-present-to-v2.md +49 -0
- package/examples/v2/activity-events-snapshots.json +31 -0
- package/examples/v2/employees-discovery.json +26 -0
- package/examples/v2/employees-snapshots.json +27 -0
- package/examples/v2/procedures-knowledge.json +27 -0
- package/examples/v2/work-items-analysis.json +26 -0
- package/examples/v2/work-items-context.json +32 -0
- package/examples/v2/work-items-decisions.json +26 -0
- package/examples/v2/work-items-raw.json +37 -0
- package/examples/v2/work-items-relationships-candidate.json +38 -0
- package/examples/v2/work-items-snapshots-lifecycle.json +31 -0
- package/examples/v2/work-items-snapshots-lightweight-refs.json +34 -0
- package/examples/v2/work-items-snapshots.json +28 -0
- package/examples/v2/work-items-workflow.json +28 -0
- package/package.json +7 -2
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { MEMORIX_RECORD_FORMAT_V2, } from "./types.js";
|
|
2
|
+
import { DATA_CATEGORIES, RECORD_STATES, V2_FIXED_ROOTS, V2_SNAPSHOT_BUCKETS, resolveContentProfile, } from "./profiles.js";
|
|
3
|
+
function issue(path, code, message, severity = "error") {
|
|
4
|
+
return { path, code, message, severity };
|
|
5
|
+
}
|
|
6
|
+
function isPlainObject(v) {
|
|
7
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
8
|
+
}
|
|
9
|
+
function isNonEmptyObject(v) {
|
|
10
|
+
return isPlainObject(v) && Object.keys(v).length > 0;
|
|
11
|
+
}
|
|
12
|
+
function readPath(obj, path) {
|
|
13
|
+
const parts = path.split(".");
|
|
14
|
+
let cur = obj;
|
|
15
|
+
for (const p of parts) {
|
|
16
|
+
if (!isPlainObject(cur))
|
|
17
|
+
return undefined;
|
|
18
|
+
cur = cur[p];
|
|
19
|
+
}
|
|
20
|
+
return cur;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate a memorix-record/2 document.
|
|
24
|
+
* Empty optional snapshot buckets must be omitted (not `{}`).
|
|
25
|
+
*/
|
|
26
|
+
export function validateMemorixRecordV2(input, options = {}) {
|
|
27
|
+
const issues = [];
|
|
28
|
+
if (!isPlainObject(input)) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
issues: [issue("", "V2-TYPE", "Record must be a plain object")],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const rejectUnknown = options.rejectUnknownRoots !== false;
|
|
35
|
+
const profile = resolveContentProfile(options.contentType ?? (typeof input.contentType === "string" ? input.contentType : undefined), options.profile);
|
|
36
|
+
const allowed = new Set([
|
|
37
|
+
...V2_FIXED_ROOTS,
|
|
38
|
+
...(profile.allowSnapshotBuckets ? V2_SNAPSHOT_BUCKETS : []),
|
|
39
|
+
...(profile.allowedExtraRoots ?? []),
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Option A (Gate G2): root `associated*` may hold lightweight relationship
|
|
43
|
+
* references only. Copied target payloads are rejected.
|
|
44
|
+
* Gate G4 selected authoritative storage = central per-category collections
|
|
45
|
+
* (Candidate C). Option A roots remain format-valid but are not authoritative storage.
|
|
46
|
+
*/
|
|
47
|
+
const LIGHTWEIGHT_REF_KEYS = new Set([
|
|
48
|
+
"recordId",
|
|
49
|
+
"relationshipId",
|
|
50
|
+
"relationshipDefinitionId",
|
|
51
|
+
]);
|
|
52
|
+
for (const key of Object.keys(input)) {
|
|
53
|
+
if (key.startsWith("associated")) {
|
|
54
|
+
const value = input[key];
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
if (!Array.isArray(value)) {
|
|
58
|
+
issues.push(issue(key, "V2-ASSOC-TYPE", `${key} must be an array of lightweight relationship refs`));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (value.length === 0) {
|
|
62
|
+
issues.push(issue(key, "V2-ASSOC-EMPTY", `${key} must be omitted when empty`));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < value.length; i++) {
|
|
66
|
+
const item = value[i];
|
|
67
|
+
const path = `${key}[${i}]`;
|
|
68
|
+
if (!isPlainObject(item)) {
|
|
69
|
+
issues.push(issue(path, "V2-ASSOC-ITEM", `${path} must be a plain object`));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (typeof item.recordId !== "string" || !item.recordId.trim()) {
|
|
73
|
+
issues.push(issue(`${path}.recordId`, "V2-ASSOC-RECORD-ID", `${path}.recordId is required`));
|
|
74
|
+
}
|
|
75
|
+
if (typeof item.relationshipDefinitionId !== "string" ||
|
|
76
|
+
!item.relationshipDefinitionId.trim()) {
|
|
77
|
+
issues.push(issue(`${path}.relationshipDefinitionId`, "V2-ASSOC-DEF-ID", `${path}.relationshipDefinitionId is required — without it this is a record reference, not a relationship`));
|
|
78
|
+
}
|
|
79
|
+
for (const k of Object.keys(item)) {
|
|
80
|
+
if (!LIGHTWEIGHT_REF_KEYS.has(k)) {
|
|
81
|
+
issues.push(issue(`${path}.${k}`, "V2-ASSOC-COPIED", `${path} may only contain relationshipDefinitionId, recordId, and optional relationshipId — copied related payload rejected (Gate G2). Option A lightweight roots remain format-valid; authoritative storage is G4 Candidate C (central per-category collections).`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (item.relationshipId !== undefined &&
|
|
85
|
+
(typeof item.relationshipId !== "string" || !String(item.relationshipId).trim())) {
|
|
86
|
+
issues.push(issue(`${path}.relationshipId`, "V2-ASSOC-REF", `${path}.relationshipId must be a non-empty string when present (optional only because it is deterministically derivable)`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (rejectUnknown && !allowed.has(key) && !key.startsWith("_")) {
|
|
92
|
+
issues.push(issue(key, "V2-ROOT", `Unknown root '${key}' is not allowed for contentType '${profile.contentType}'`));
|
|
93
|
+
}
|
|
94
|
+
if (key.startsWith("_") && key !== "_system" && key !== "_id") {
|
|
95
|
+
issues.push(issue(key, "V2-SYSTEM-ROOT", `Only _system and _id machine roots are allowed; found '${key}'`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (input.format !== MEMORIX_RECORD_FORMAT_V2) {
|
|
99
|
+
issues.push(issue("format", "V2-FORMAT", `format must be "${MEMORIX_RECORD_FORMAT_V2}"`));
|
|
100
|
+
}
|
|
101
|
+
if (typeof input.recordId !== "string" || !input.recordId.trim()) {
|
|
102
|
+
issues.push(issue("recordId", "V2-RECORD-ID", "recordId must be a non-empty string"));
|
|
103
|
+
}
|
|
104
|
+
if (typeof input.revision !== "number" || !Number.isInteger(input.revision) || input.revision < 1) {
|
|
105
|
+
issues.push(issue("revision", "V2-REVISION", "revision must be an integer >= 1"));
|
|
106
|
+
}
|
|
107
|
+
if (typeof input.objectType !== "string" || !input.objectType.trim()) {
|
|
108
|
+
issues.push(issue("objectType", "V2-OBJECT-TYPE", "objectType must be a non-empty string"));
|
|
109
|
+
}
|
|
110
|
+
else if (options.objectType && input.objectType !== options.objectType) {
|
|
111
|
+
issues.push(issue("objectType", "V2-OBJECT-TYPE-MISMATCH", `objectType '${input.objectType}' does not match expected '${options.objectType}'`));
|
|
112
|
+
}
|
|
113
|
+
if (typeof input.contentType !== "string" || !input.contentType.trim()) {
|
|
114
|
+
issues.push(issue("contentType", "V2-CONTENT-TYPE", "contentType must be a non-empty string"));
|
|
115
|
+
}
|
|
116
|
+
else if (options.contentType && input.contentType !== options.contentType) {
|
|
117
|
+
issues.push(issue("contentType", "V2-CONTENT-TYPE-MISMATCH", `contentType '${input.contentType}' does not match expected '${options.contentType}'`));
|
|
118
|
+
}
|
|
119
|
+
if (typeof input.dataCategory !== "string" ||
|
|
120
|
+
!DATA_CATEGORIES.includes(input.dataCategory)) {
|
|
121
|
+
issues.push(issue("dataCategory", "V2-DATA-CATEGORY", `dataCategory must be one of ${DATA_CATEGORIES.join(", ")}`));
|
|
122
|
+
}
|
|
123
|
+
if (!isPlainObject(input.concept)) {
|
|
124
|
+
issues.push(issue("concept", "V2-CONCEPT", "concept must be a plain object"));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (typeof input.concept.title !== "string" || !input.concept.title.trim()) {
|
|
128
|
+
issues.push(issue("concept.title", "V2-CONCEPT-TITLE", "concept.title must be a non-empty string"));
|
|
129
|
+
}
|
|
130
|
+
const identifiers = input.concept.identifiers;
|
|
131
|
+
if (!isPlainObject(identifiers)) {
|
|
132
|
+
issues.push(issue("concept.identifiers", "V2-IDENTIFIERS", "concept.identifiers is required"));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const primary = identifiers.primary;
|
|
136
|
+
if (!isPlainObject(primary)) {
|
|
137
|
+
issues.push(issue("concept.identifiers.primary", "V2-PRIMARY", "primary identifier is required"));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (typeof primary.kind !== "string" || !primary.kind.trim()) {
|
|
141
|
+
issues.push(issue("concept.identifiers.primary.kind", "V2-PRIMARY-KIND", "kind is required"));
|
|
142
|
+
}
|
|
143
|
+
if (typeof primary.value !== "string" || !primary.value.trim()) {
|
|
144
|
+
issues.push(issue("concept.identifiers.primary.value", "V2-PRIMARY-VALUE", "value is required"));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (identifiers.alternates !== undefined) {
|
|
148
|
+
if (!Array.isArray(identifiers.alternates)) {
|
|
149
|
+
issues.push(issue("concept.identifiers.alternates", "V2-ALTERNATES", "alternates must be an array"));
|
|
150
|
+
}
|
|
151
|
+
else if (identifiers.alternates.length === 0) {
|
|
152
|
+
issues.push(issue("concept.identifiers.alternates", "V2-ALTERNATES-EMPTY", "alternates must be omitted when empty"));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!isPlainObject(input.data)) {
|
|
158
|
+
issues.push(issue("data", "V2-DATA", "data must be a plain object"));
|
|
159
|
+
}
|
|
160
|
+
for (const bucket of V2_SNAPSHOT_BUCKETS) {
|
|
161
|
+
if (input[bucket] === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
if (!profile.allowSnapshotBuckets) {
|
|
164
|
+
issues.push(issue(bucket, "V2-SCOPE-BUCKET", `${bucket} is only allowed on snapshot-like content types (got '${profile.contentType}')`));
|
|
165
|
+
}
|
|
166
|
+
else if (!isNonEmptyObject(input[bucket])) {
|
|
167
|
+
issues.push(issue(bucket, "V2-EMPTY-BUCKET", `${bucket} must be omitted when empty (do not emit {})`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!isPlainObject(input._system)) {
|
|
171
|
+
issues.push(issue("_system", "V2-SYSTEM", "_system is required"));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const sys = input._system;
|
|
175
|
+
if (typeof sys.state !== "string" ||
|
|
176
|
+
!RECORD_STATES.includes(sys.state)) {
|
|
177
|
+
issues.push(issue("_system.state", "V2-STATE", `state must be one of ${RECORD_STATES.join(", ")}`));
|
|
178
|
+
}
|
|
179
|
+
for (const ts of ["createdAt", "modifiedAt"]) {
|
|
180
|
+
if (typeof sys[ts] !== "string" || !sys[ts].trim()) {
|
|
181
|
+
issues.push(issue(`_system.${ts}`, "V2-TIMESTAMP", `${ts} must be a non-empty string`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (sys.lifecycle !== undefined) {
|
|
185
|
+
if (!Array.isArray(sys.lifecycle)) {
|
|
186
|
+
issues.push(issue("_system.lifecycle", "V2-LIFECYCLE", "lifecycle must be an array"));
|
|
187
|
+
}
|
|
188
|
+
else if (sys.lifecycle.length === 0) {
|
|
189
|
+
issues.push(issue("_system.lifecycle", "V2-LIFECYCLE-EMPTY", "lifecycle must be omitted when empty"));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (sys.provenance !== undefined && !isNonEmptyObject(sys.provenance)) {
|
|
193
|
+
issues.push(issue("_system.provenance", "V2-PROVENANCE-EMPTY", "provenance must be omitted when empty"));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (options.enforceDiscriminator !== false && profile.discriminator && isPlainObject(input)) {
|
|
197
|
+
const disc = profile.discriminator;
|
|
198
|
+
const value = readPath(input, disc.path);
|
|
199
|
+
if (value === undefined || value === null || value === "") {
|
|
200
|
+
if (disc.defaultValue !== undefined) {
|
|
201
|
+
// Metadata-defined default exists: structural validation passes; writers may apply default.
|
|
202
|
+
issues.push(issue(disc.path, "V2-DISCRIMINATOR-DEFAULT", `${disc.path} missing; content-type profile default '${disc.defaultValue}' applies at write time`, "warning"));
|
|
203
|
+
}
|
|
204
|
+
else if (disc.required) {
|
|
205
|
+
issues.push(issue(disc.path, "V2-DISCRIMINATOR", `Required discriminator ${disc.path} is missing and no default is configured`));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (typeof value !== "string") {
|
|
209
|
+
issues.push(issue(disc.path, "V2-DISCRIMINATOR-TYPE", `${disc.path} must be a string`));
|
|
210
|
+
}
|
|
211
|
+
else if (disc.allowedValues && !disc.allowedValues.includes(value)) {
|
|
212
|
+
issues.push(issue(disc.path, "V2-DISCRIMINATOR-VALUE", `${disc.path}='${value}' is not in allowedValues`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const ok = !issues.some((i) => i.severity === "error");
|
|
216
|
+
return {
|
|
217
|
+
ok,
|
|
218
|
+
issues,
|
|
219
|
+
record: ok ? input : undefined,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function validateMemorixRecordsV2(inputs, options = {}) {
|
|
223
|
+
return inputs.map((input) => validateMemorixRecordV2(input, options));
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/v2/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -38,6 +38,9 @@ Object-type segments come from Catalox descriptors — not from this format pack
|
|
|
38
38
|
## Payload rules
|
|
39
39
|
|
|
40
40
|
- Associated properties are **arrays of plain objects** (linked payloads), never full envelopes.
|
|
41
|
+
- On **memorix-record/2**, root `associated*` may hold **lightweight refs only** (Option A — format-valid): `relationshipDefinitionId` + `recordId`, optional `relationshipId`. Copied related business payload is rejected.
|
|
42
|
+
- **Authoritative relationship storage (Gate G4)** is centralized per-category collections (Candidate C), not Option A roots or sibling `relationships` content. See `docs/re-factoring-and-ops-agent/RELATIONSHIP-STORAGE-DECISION.md`.
|
|
43
|
+
|
|
41
44
|
- **Omit** the property when empty — never write `[]`.
|
|
42
45
|
- Prefer traceability in optional item-level `_system.association` (v2) rather than wrapping envelopes.
|
|
43
46
|
|
package/docs/memorix-format.md
CHANGED
|
@@ -1,55 +1,50 @@
|
|
|
1
1
|
# Memorix Record Format
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
3
|
+
> **Canonical SoT for new work:** [memorix-record/2](./memorix-record-v2.md) (`validateMemorixRecordV2`).
|
|
4
|
+
> **Present envelope (migration source):** documented below — still enforced by default in writer/pipeline until `MEMORIX_RECORD_FORMAT=v2`.
|
|
5
|
+
> Core present envelope helpers: [`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format) (temporary; folding into this package).
|
|
6
|
+
> Requirements: [memorix-format-requirements.md](./memorix-format-requirements.md).
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
9
9
|
|
|
10
10
|
A Memorix record is a structured memory object: a stable envelope around domain payload, with clear separation between business data, derived analytics, external enrichment, AI insights, and cross-entity references.
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**Prefer `memorix-record/2` for all new platform paths.** Present envelope remains for reading/migrating existing data.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Present envelope (migration source)
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
type
|
|
20
|
-
_id?: string;
|
|
21
|
-
recordId: string;
|
|
19
|
+
type MemorixRecordPresent = {
|
|
20
|
+
_id?: string;
|
|
21
|
+
recordId: string;
|
|
22
22
|
concept: {
|
|
23
23
|
_extIdentifier: { kind: string; key: string; [kindValue: string]: string };
|
|
24
24
|
title: string;
|
|
25
|
-
// other human/LLM-readable fields allowed
|
|
26
25
|
};
|
|
27
|
-
data: JsonObject;
|
|
28
|
-
|
|
26
|
+
data: JsonObject;
|
|
29
27
|
analytics?: JsonObject;
|
|
30
28
|
enrichment?: JsonObject;
|
|
31
29
|
insights?: JsonObject;
|
|
32
|
-
// associated…?: JsonObject[];
|
|
33
|
-
|
|
30
|
+
// associated…?: JsonObject[]; // copied payloads — removed in V2
|
|
34
31
|
_system?: {
|
|
35
32
|
state?: "active" | "archived" | "superseded";
|
|
36
33
|
lifecycle?: Array<{ process: string; processedAt: string }>;
|
|
37
|
-
// …
|
|
38
34
|
};
|
|
39
35
|
};
|
|
40
36
|
```
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Human/LLM readers SHOULD omit root `_…` and `concept._extIdentifier` by default; keep `recordId` for joins.
|
|
38
|
+
Migrate legacy → present: `npx memorix-format migrate`.
|
|
39
|
+
Migrate present → V2: `npx memorix-format migrate-to-v2 --object-type=… --content-type=… --data-category=…`.
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
Associated properties use the reserved `associated` prefix on **present** records only. See [association-format.md](./association-format.md). V2 rejects copied association roots.
|
|
47
42
|
|
|
48
|
-
|
|
43
|
+
Human/LLM readers SHOULD omit root `_…` and `concept._extIdentifier` by default; keep `recordId` for joins.
|
|
49
44
|
|
|
50
45
|
---
|
|
51
46
|
|
|
52
|
-
## Data And Associated Buckets
|
|
47
|
+
## Data And Associated Buckets (present)
|
|
53
48
|
|
|
54
49
|
Every piece of business data lives in **exactly one** of these buckets (plus the envelope fields above):
|
|
55
50
|
|
|
@@ -58,173 +53,16 @@ Every piece of business data lives in **exactly one** of these buckets (plus the
|
|
|
58
53
|
| **`data`** | Native facts about the subject itself |
|
|
59
54
|
| **`analytics`** | Numeric rollups computed from other records |
|
|
60
55
|
| **`enrichment`** | External / prior-system values as received |
|
|
61
|
-
| **`insights`** |
|
|
62
|
-
| **`
|
|
63
|
-
| **`associatedDiscovery`** | Linked discovery content copied from associated records' `data` payloads |
|
|
64
|
-
| **`associatedAnalysis`** | Linked analysis content copied from associated records' `data` payloads |
|
|
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
|
-
|
|
68
|
-
Do not duplicate a value across buckets. When reshaping legacy records, promote `sourceFacts` values into the correct bucket rather than wrapping them.
|
|
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`, and so on.
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Concept
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
type MemorixConcept = {
|
|
88
|
-
_extIdentifier: {
|
|
89
|
-
kind: string;
|
|
90
|
-
/** Primary value — property name matches `kind`. */
|
|
91
|
-
[kind: string]: string;
|
|
92
|
-
/**
|
|
93
|
-
* Deterministic join key: `kind` first, then the kind-named value,
|
|
94
|
-
* then any other scalar fields in sorted key order, joined by `:`.
|
|
95
|
-
*/
|
|
96
|
-
key: string;
|
|
97
|
-
};
|
|
98
|
-
title: string;
|
|
99
|
-
};
|
|
100
|
-
```
|
|
101
|
-
|
|
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.
|
|
103
|
-
|
|
104
|
-
```json
|
|
105
|
-
"concept": {
|
|
106
|
-
"_extIdentifier": {
|
|
107
|
-
"kind": "sku",
|
|
108
|
-
"sku": "SKU-100",
|
|
109
|
-
"key": "sku:SKU-100"
|
|
110
|
-
},
|
|
111
|
-
"title": "Blue widget"
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
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.
|
|
116
|
-
|
|
117
|
-
### Identifier `kind` (illustrative — not format law)
|
|
118
|
-
|
|
119
|
-
| Object type (abstract) | `kind` | Example |
|
|
120
|
-
|---|---|---|
|
|
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"}` |
|
|
125
|
-
|
|
126
|
-
Domain packs (Catalox) may use different `kind` values; those recipes live on descriptors.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## `_system`
|
|
131
|
-
|
|
132
|
-
System metadata — provenance, processing history, graph-run stamps — never belongs in `data` or the other business buckets.
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
type MemorixLifecycleEvent = { process: string; processedAt: string };
|
|
136
|
-
|
|
137
|
-
type MemorixSystemBlock = {
|
|
138
|
-
state?: "active" | "archived" | "superseded";
|
|
139
|
-
lifecycle?: MemorixLifecycleEvent[];
|
|
140
|
-
_graphRuns?: Record<string, GraphRunEntry>;
|
|
141
|
-
_jobTypeRuns?: Record<string, GraphRunEntry>;
|
|
142
|
-
provenance?: JsonObject;
|
|
143
|
-
// other system keys as needed (memorix, scoping, …)
|
|
144
|
-
};
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Lifecycle
|
|
148
|
-
|
|
149
|
-
Root-level `lifecycle` is gone. Processing events live at `_system.lifecycle`:
|
|
150
|
-
|
|
151
|
-
```json
|
|
152
|
-
"lifecycle": [
|
|
153
|
-
{ "process": "enriched", "processedAt": "2026-04-05T08:01:15.511Z" }
|
|
154
|
-
]
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
- Each entry is `{ process, processedAt }`.
|
|
158
|
-
- When `createdAt`, `modifiedAt`, and `capturedAt` are identical (the common snapshot case), emit a single `"enriched"` event rather than three duplicate timestamps.
|
|
159
|
-
- Distinct events (e.g. a future re-enrichment) append as additional array entries.
|
|
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`).
|
|
162
|
-
|
|
163
|
-
`_system.provenance.enrichedAt` (when present) is separate provenance metadata about a prior enrichment step, not a substitute for `lifecycle`.
|
|
164
|
-
|
|
165
|
-
### Graph and job-type runs
|
|
166
|
-
|
|
167
|
-
`_graphRuns` and `_jobTypeRuns` are stored under `_system`, keyed by graph id and sanitized job-type id respectively. Each entry tracks execution status, timestamps, and result metadata. These fields are written by Memorix writer during graph execution and are not part of the domain payload.
|
|
168
|
-
|
|
169
|
-
Dot-path references: `_system._graphRuns.<graphId>`, `_system._jobTypeRuns.<jobTypeId>`.
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
## Complete example
|
|
174
|
-
|
|
175
|
-
Abstract item snapshot showing envelope fields and open buckets:
|
|
176
|
-
|
|
177
|
-
```json
|
|
178
|
-
{
|
|
179
|
-
"recordId": "rec-item-1",
|
|
180
|
-
"concept": {
|
|
181
|
-
"_extIdentifier": {
|
|
182
|
-
"kind": "sku",
|
|
183
|
-
"sku": "SKU-100",
|
|
184
|
-
"key": "sku:SKU-100"
|
|
185
|
-
},
|
|
186
|
-
"title": "Blue widget"
|
|
187
|
-
},
|
|
188
|
-
"data": {
|
|
189
|
-
"sku": "SKU-100",
|
|
190
|
-
"color": "blue",
|
|
191
|
-
"family": "widgets"
|
|
192
|
-
},
|
|
193
|
-
"associatedData": [
|
|
194
|
-
{ "locationCode": "WH-A", "qty": 12 },
|
|
195
|
-
{ "locationCode": "WH-B", "qty": 3 }
|
|
196
|
-
],
|
|
197
|
-
"analytics": {
|
|
198
|
-
"linkedLocations": { "total": 2 }
|
|
199
|
-
},
|
|
200
|
-
"enrichment": {
|
|
201
|
-
"catalogScore": 0.91,
|
|
202
|
-
"sourceSystem": "erp-feed"
|
|
203
|
-
},
|
|
204
|
-
"insights": {
|
|
205
|
-
"priority": { "level": "high", "confidence": 0.9 }
|
|
206
|
-
},
|
|
207
|
-
"_system": {
|
|
208
|
-
"state": "active",
|
|
209
|
-
"provenance": {
|
|
210
|
-
"source": {
|
|
211
|
-
"database": "example-db",
|
|
212
|
-
"collection": "items-rawdata"
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
"lifecycle": [
|
|
216
|
-
{ "process": "enriched", "processedAt": "2026-04-05T08:01:15.511Z" }
|
|
217
|
-
]
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
```
|
|
56
|
+
| **`insights`** | AI / judgment outputs |
|
|
57
|
+
| **`associated*`** | Copied related payloads (present only; removed in V2) |
|
|
221
58
|
|
|
222
|
-
|
|
59
|
+
On **memorix-record/2**, snapshot buckets are optional and **omitted when empty**. Root `associated*` may hold **lightweight refs** only (Option A — still format-valid). **Authoritative relationship storage (Gate G4)** is centralized per-category collections (Candidate C); see `docs/re-factoring-and-ops-agent/RELATIONSHIP-STORAGE-DECISION.md`.
|
|
223
60
|
|
|
224
61
|
---
|
|
225
62
|
|
|
226
|
-
##
|
|
63
|
+
## Further reading
|
|
227
64
|
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
65
|
+
- [memorix-record-v2.md](./memorix-record-v2.md) — Gate G2 contract
|
|
66
|
+
- [association-format.md](./association-format.md) — present association naming
|
|
67
|
+
- [memorix-workflow-format.md](./memorix-workflow-format.md) — workflow as sibling content
|
|
68
|
+
- Golden V2 examples: `examples/v2/`
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# memorix-record/2
|
|
2
|
+
|
|
3
|
+
**Status:** Gate G2 contract — preferred source of truth for new platform work.
|
|
4
|
+
**Package:** `@x12i/memorix-format` (`validateMemorixRecordV2`, `migratePresentToV2`, CLI `validate-v2` / `migrate-to-v2`).
|
|
5
|
+
|
|
6
|
+
Present-tense envelope docs remain in [memorix-format.md](./memorix-format.md) for migration only.
|
|
7
|
+
Migration semantics: [migrate-present-to-v2.md](./migrate-present-to-v2.md).
|
|
8
|
+
|
|
9
|
+
## Envelope
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
type MemorixRecordV2 = {
|
|
13
|
+
format: "memorix-record/2";
|
|
14
|
+
recordId: string;
|
|
15
|
+
revision: number; // starts at 1; increments every successful mutation
|
|
16
|
+
objectType: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
dataCategory: "entity" | "event" | "knowledge";
|
|
19
|
+
concept: {
|
|
20
|
+
title: string;
|
|
21
|
+
identifiers: {
|
|
22
|
+
primary: { kind: string; value: string; issuer?: string; normalizedValue?: string };
|
|
23
|
+
alternates?: Array<...>; // omit when empty
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
data: Record<string, unknown>;
|
|
27
|
+
analytics?: Record<string, unknown>; // snapshots (and knowledge) only; omit when unused
|
|
28
|
+
enrichment?: Record<string, unknown>;
|
|
29
|
+
insights?: Record<string, unknown>;
|
|
30
|
+
// Option A — lightweight relationship refs remain FORMAT-VALID (G2).
|
|
31
|
+
// Gate G4 selected authoritative storage = central per-category collections (Candidate C).
|
|
32
|
+
// associatedPeople?: Array<{
|
|
33
|
+
// relationshipDefinitionId: string; // required
|
|
34
|
+
// recordId: string; // required
|
|
35
|
+
// relationshipId?: string; // optional — deterministically derivable
|
|
36
|
+
// }>;
|
|
37
|
+
_system: {
|
|
38
|
+
state: "active" | "archived" | "superseded";
|
|
39
|
+
createdAt: string;
|
|
40
|
+
modifiedAt: string;
|
|
41
|
+
observedAt?: string;
|
|
42
|
+
lifecycle?: Array<{ process: string; processedAt: string }>; // omit when empty
|
|
43
|
+
provenance?: Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Locked decisions (Gate G2)
|
|
49
|
+
|
|
50
|
+
1. **Empty optional snapshot buckets are omitted** — never emit `{}`.
|
|
51
|
+
2. **Workflow is a sibling content record** (`contentType: "workflow"`), not a separate document system.
|
|
52
|
+
3. **Composition** = same `recordId` across content types; not a relationship.
|
|
53
|
+
4. **`_graphRuns` / `_jobTypeRuns` stay off the subject** — operational run ledger owns run history.
|
|
54
|
+
5. **V2 rejects copied related-record payloads.** Root `associated*` may hold **lightweight relationship refs** only: required `relationshipDefinitionId` + `recordId`; optional `relationshipId` (derivable). Without `relationshipDefinitionId` the item is not a relationship. **Option A** (direct-record lightweight `associated*` roots) remains **format-valid** for validation and migration compatibility.
|
|
55
|
+
6. **Authoritative physical relationship storage is Gate G4 Candidate C** — org-prefixed per-category `relationships` collections (`entity` / `event` / `knowledge`). Option A lightweight roots and sibling `relationships` content are **not** the selected authoritative storage model; see `docs/re-factoring-and-ops-agent/RELATIONSHIP-STORAGE-DECISION.md`.
|
|
56
|
+
7. **Present format remains the writer/pipeline default** until cutover (`MEMORIX_RECORD_FORMAT=v2`). All **new** refactor packages and the `ops` harness use V2 exclusively. The env switch is transitional (`DEL-FORMAT-PRESENT-SOT`).
|
|
57
|
+
|
|
58
|
+
## Revision vs lifecycle
|
|
59
|
+
|
|
60
|
+
| Rule | Behavior |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Revision | Increments on every successful mutation |
|
|
63
|
+
| Lifecycle | Appended only for meaningful process/state events |
|
|
64
|
+
| Independence | Revision may change with no lifecycle entry |
|
|
65
|
+
| Concurrency | Stale `expectedRevision` is rejected (`checkExpectedRevision`) |
|
|
66
|
+
|
|
67
|
+
Helpers: `applySuccessfulMutation`, `nextRevision`, `checkExpectedRevision`.
|
|
68
|
+
|
|
69
|
+
## Discriminators
|
|
70
|
+
|
|
71
|
+
- Pure structural V2 validation does not invent discriminator values.
|
|
72
|
+
- Content-type **profiles** (from metadata) may declare `path`, `required`, `defaultValue`, `allowedValues`.
|
|
73
|
+
- Missing + profile default → warning `V2-DISCRIMINATOR-DEFAULT` (writers apply default).
|
|
74
|
+
- Missing + required + no default → error `V2-DISCRIMINATOR`.
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
See [`examples/v2/`](../examples/v2/) — includes lifecycle, lightweight refs, analysis/decision/discovery, workflow sibling, relationships-candidate sibling.
|
|
79
|
+
|
|
80
|
+
## Migrate
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx memorix-format migrate-to-v2 ./present.json \
|
|
84
|
+
--object-type=work-items \
|
|
85
|
+
--content-type=snapshots \
|
|
86
|
+
--data-category=entity \
|
|
87
|
+
--issuer=ops-source
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Output includes `v2`, `extractedRelationships`, and `warnings`.
|
|
91
|
+
|
|
92
|
+
## Writer cutover flag
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
MEMORIX_RECORD_FORMAT=v2 # writer uses validateMemorixRecordV2
|
|
96
|
+
# default: present (legacy paths only)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Folding `@x12i/memorix-base-format`
|
|
100
|
+
|
|
101
|
+
Present base validation still depends on `@x12i/memorix-base-format`. That package becomes a temporary re-export after consumers move to `@x12i/memorix-format` / V2, then is deleted (Phase 0 deletion ledger).
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
# Memorix Workflow Format
|
|
1
|
+
# Memorix Workflow Format
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
+
**Gate G2:** Workflow is a **sibling content record** on `memorix-record/2` with
|
|
6
|
+
`contentType: "workflow"` and the same `recordId` as the subject — not a separate
|
|
7
|
+
document system. See [memorix-record-v2.md](./memorix-record-v2.md) and
|
|
8
|
+
`examples/v2/work-items-workflow.json`.
|
|
9
|
+
|
|
10
|
+
Legacy product shape (`memorix-workflow/1`) below remains for migration until
|
|
11
|
+
workflow-api cutover.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Legacy: `memorix-workflow/1`
|
|
16
|
+
|
|
5
17
|
Versioned JSON for **product workflow state** attached to a subject object via the
|
|
6
18
|
content type `workflow` (collection `{objectType}-workflow`).
|
|
7
19
|
|