@x12i/memorix-format 1.0.0 → 2.1.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.
- package/CHANGELOG.md +22 -0
- package/README.md +27 -63
- package/dist/associated.d.ts +12 -12
- package/dist/associated.d.ts.map +1 -1
- package/dist/associated.js +26 -33
- package/dist/associated.js.map +1 -1
- package/dist/cli.js +129 -77
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/load-meta.d.ts +56 -0
- package/dist/load-meta.d.ts.map +1 -0
- package/dist/load-meta.js +94 -0
- package/dist/load-meta.js.map +1 -0
- package/dist/profiles.d.ts +15 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +47 -0
- package/dist/profiles.js.map +1 -0
- package/dist/types.d.ts +8 -80
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +17 -9
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +43 -184
- package/dist/validate.js.map +1 -1
- package/docs/memorix-format-requirements.md +52 -0
- package/docs/memorix-format.md +3 -1
- package/examples/valid-snapshot.json +3 -12
- package/meta/associated-conventions.json +59 -0
- package/meta/content-type-profiles.json +27 -0
- package/meta/content-types.json +68 -0
- package/meta/inventory/managed-descriptors.json +77 -0
- package/meta/open-buckets.json +24 -0
- package/package.json +14 -6
- package/dist/concept.d.ts +0 -16
- package/dist/concept.d.ts.map +0 -1
- package/dist/concept.js +0 -75
- package/dist/concept.js.map +0 -1
- package/dist/constants.d.ts +0 -21
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -49
- package/dist/constants.js.map +0 -1
- package/dist/inspect.d.ts +0 -4
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -51
- package/dist/inspect.js.map +0 -1
- package/dist/utils.d.ts +0 -6
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -23
- package/dist/utils.js.map +0 -1
package/dist/validate.js
CHANGED
|
@@ -1,211 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isAssociatedPropertyName, isLegacyAssociatedPropertyName } from "./associated.js";
|
|
3
|
-
import {
|
|
4
|
-
import { isPlainObject, stringifyId } from "./utils.js";
|
|
1
|
+
import { validateMemorixBaseRecord, } from "@x12i/memorix-base-format";
|
|
2
|
+
import { isAssociatedPropertyName, isLegacyAssociatedPropertyName, KNOWN_OPEN_BUCKETS, } from "./associated.js";
|
|
3
|
+
import { resolveMemorixProfile } from "./profiles.js";
|
|
5
4
|
function issue(path, code, message, severity = "error") {
|
|
6
5
|
return { path, code, message, severity };
|
|
7
6
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
issues.push(issue("concept", "concept.required", "concept must be a plain object"));
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
const title = stringifyId(concept.title);
|
|
14
|
-
if (!title) {
|
|
15
|
-
issues.push(issue("concept.title", "concept.title.required", "concept.title must be a non-empty string"));
|
|
16
|
-
}
|
|
17
|
-
if (concept.name !== undefined) {
|
|
18
|
-
issues.push(issue("concept.name", "concept.name.legacy", 'concept.name is removed in the clean format; use concept.identifier + concept.title', "warning"));
|
|
19
|
-
}
|
|
20
|
-
const identifier = concept.identifier;
|
|
21
|
-
if (!isPlainObject(identifier)) {
|
|
22
|
-
issues.push(issue("concept.identifier", "concept.identifier.required", "concept.identifier must be a plain object"));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
const kind = stringifyId(identifier.kind);
|
|
26
|
-
if (!kind) {
|
|
27
|
-
issues.push(issue("concept.identifier.kind", "concept.identifier.kind.required", "concept.identifier.kind must be a non-empty string"));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (identifier[kind] == null || String(identifier[kind]).trim() === "") {
|
|
31
|
-
issues.push(issue(`concept.identifier.${kind}`, "concept.identifier.value.required", `concept.identifier.${kind} must be present for kind "${kind}"`));
|
|
32
|
-
}
|
|
33
|
-
const identifierKey = stringifyId(identifier.identifierKey);
|
|
34
|
-
if (!identifierKey) {
|
|
35
|
-
issues.push(issue("concept.identifier.identifierKey", "concept.identifier.identifierKey.required", "concept.identifier.identifierKey is required"));
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
const expected = buildIdentityString(identifier);
|
|
39
|
-
if (expected && identifierKey !== expected) {
|
|
40
|
-
issues.push(issue("concept.identifier.identifierKey", "concept.identifier.identifierKey.mismatch", `identifierKey should be "${expected}", got "${identifierKey}"`, "warning"));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
for (const [key, value] of Object.entries(identifier)) {
|
|
44
|
-
if (typeof value !== "string") {
|
|
45
|
-
issues.push(issue(`concept.identifier.${key}`, "concept.identifier.value.type", `concept.identifier.${key} must be a string`));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function validateAssociatedProperty(name, value, issues) {
|
|
50
|
-
if (value === undefined)
|
|
51
|
-
return;
|
|
52
|
-
if (!Array.isArray(value)) {
|
|
53
|
-
issues.push(issue(name, "associated.array", `${name} must be an array of objects (omit when empty)`));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (value.length === 0) {
|
|
57
|
-
issues.push(issue(name, "associated.empty", `${name} should be omitted when empty`, "warning"));
|
|
58
|
-
}
|
|
59
|
-
for (let i = 0; i < value.length; i++) {
|
|
60
|
-
const item = value[i];
|
|
61
|
-
if (!isPlainObject(item)) {
|
|
62
|
-
issues.push(issue(`${name}[${i}]`, "associated.item.object", `${name}[${i}] must be a plain object`));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (isLegacyAssociatedPropertyName(name)) {
|
|
66
|
-
issues.push(issue(name, "associated.legacy_name", `${name} is a legacy associated name; prefer associated<ObjectType><ContentType>`, "warning"));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function validateObjectBucket(name, value, issues) {
|
|
70
|
-
if (value === undefined)
|
|
71
|
-
return;
|
|
72
|
-
if (!isPlainObject(value)) {
|
|
73
|
-
issues.push(issue(name, `${name}.object`, `${name} must be a plain object`));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (Object.keys(value).length === 0) {
|
|
77
|
-
issues.push(issue(name, `${name}.empty`, `${name} should be omitted when empty`, "warning"));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function validateSystem(system, issues) {
|
|
81
|
-
if (system === undefined)
|
|
82
|
-
return;
|
|
83
|
-
if (!isPlainObject(system)) {
|
|
84
|
-
issues.push(issue("_system", "system.object", "_system must be a plain object"));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const lifecycle = system.lifecycle;
|
|
88
|
-
if (lifecycle !== undefined) {
|
|
89
|
-
if (!Array.isArray(lifecycle)) {
|
|
90
|
-
issues.push(issue("_system.lifecycle", "system.lifecycle.array", "_system.lifecycle must be an array"));
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
if (lifecycle.length === 0) {
|
|
94
|
-
issues.push(issue("_system.lifecycle", "system.lifecycle.empty", "_system.lifecycle should be omitted when empty", "warning"));
|
|
95
|
-
}
|
|
96
|
-
for (let i = 0; i < lifecycle.length; i++) {
|
|
97
|
-
const entry = lifecycle[i];
|
|
98
|
-
if (!isPlainObject(entry)) {
|
|
99
|
-
issues.push(issue(`_system.lifecycle[${i}]`, "system.lifecycle.entry", `_system.lifecycle[${i}] must be an object`));
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (!stringifyId(entry.process)) {
|
|
103
|
-
issues.push(issue(`_system.lifecycle[${i}].process`, "system.lifecycle.process", "lifecycle entry requires process"));
|
|
104
|
-
}
|
|
105
|
-
if (!stringifyId(entry.processedAt)) {
|
|
106
|
-
issues.push(issue(`_system.lifecycle[${i}].processedAt`, "system.lifecycle.processedAt", "lifecycle entry requires processedAt"));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
for (const mapKey of ["_graphRuns", "_jobTypeRuns"]) {
|
|
112
|
-
const map = system[mapKey];
|
|
113
|
-
if (map !== undefined && !isPlainObject(map)) {
|
|
114
|
-
issues.push(issue(`_system.${mapKey}`, `system.${mapKey}.object`, `_system.${mapKey} must be a plain object`));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
7
|
+
function isPlainObject(v) {
|
|
8
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
117
9
|
}
|
|
118
10
|
/**
|
|
119
|
-
*
|
|
120
|
-
* Does not require Mongo or Catalox — pure shape validation.
|
|
11
|
+
* Memorix product validation = base envelope + optional profile meta + ASSOC/SCOPE rules.
|
|
121
12
|
*/
|
|
122
13
|
export function validateMemorixRecord(input, options = {}) {
|
|
123
|
-
const
|
|
14
|
+
const meta = options.meta ??
|
|
15
|
+
(options.profile
|
|
16
|
+
? resolveMemorixProfile(options.profile, options.objectType ?? "generic")
|
|
17
|
+
: undefined);
|
|
18
|
+
const base = validateMemorixBaseRecord(input, {
|
|
19
|
+
...options,
|
|
20
|
+
meta,
|
|
21
|
+
});
|
|
22
|
+
const issues = [...base.issues];
|
|
124
23
|
if (!isPlainObject(input)) {
|
|
125
|
-
return
|
|
126
|
-
ok: false,
|
|
127
|
-
issues: [issue("$", "record.object", "record must be a plain JSON object")],
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
const record = input;
|
|
131
|
-
const recordId = readRecordId(record) || undefined;
|
|
132
|
-
const rootRecordId = stringifyId(record.recordId);
|
|
133
|
-
if (!rootRecordId) {
|
|
134
|
-
issues.push(issue("recordId", "recordId.required", "recordId must be a non-empty string"));
|
|
24
|
+
return base;
|
|
135
25
|
}
|
|
136
|
-
if (!isPlainObject(record.data)) {
|
|
137
|
-
issues.push(issue("data", "data.required", "data must be a plain object"));
|
|
138
|
-
}
|
|
139
|
-
validateConcept(record.concept, issues);
|
|
140
|
-
validateObjectBucket(ANALYTICS_FIELD, record.analytics, issues);
|
|
141
|
-
validateObjectBucket(ENRICHMENT_FIELD, record.enrichment, issues);
|
|
142
|
-
validateObjectBucket(INSIGHTS_FIELD, record.insights, issues);
|
|
143
|
-
validateSystem(record._system, issues);
|
|
144
26
|
const contentType = options.contentType?.trim();
|
|
145
27
|
const isSnapshot = !contentType || contentType === "snapshots";
|
|
146
|
-
|
|
147
|
-
for (const bucket of SNAPSHOT_ONLY_BUCKETS) {
|
|
148
|
-
if (record[bucket] !== undefined) {
|
|
149
|
-
issues.push(issue(bucket, "snapshot_only.bucket", `${bucket} is snapshot-only by default (contentType=${contentType})`));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
for (const key of Object.keys(record)) {
|
|
28
|
+
for (const key of Object.keys(input)) {
|
|
154
29
|
if (isAssociatedPropertyName(key)) {
|
|
30
|
+
const value = input[key];
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
if (!Array.isArray(value)) {
|
|
33
|
+
issues.push(issue(key, "ASSOC-2", `${key} MUST be an array`));
|
|
34
|
+
}
|
|
35
|
+
else if (value.length === 0) {
|
|
36
|
+
issues.push(issue(key, "ASSOC-3", `${key} MUST be omitted when empty`));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
for (let i = 0; i < value.length; i++) {
|
|
40
|
+
if (!isPlainObject(value[i])) {
|
|
41
|
+
issues.push(issue(`${key}[${i}]`, "ASSOC-5", `${key}[${i}] must be a plain object`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (isLegacyAssociatedPropertyName(key)) {
|
|
46
|
+
issues.push(issue(key, "ASSOC-1", `${key} is a legacy associated name; prefer associated<ObjectType><ContentType>`, "warning"));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
155
49
|
if (!isSnapshot) {
|
|
156
|
-
issues.push(issue(key, "
|
|
50
|
+
issues.push(issue(key, "SCOPE-2", `${key} is snapshot-oriented by default (contentType=${contentType})`));
|
|
157
51
|
}
|
|
158
|
-
validateAssociatedProperty(key, record[key], issues);
|
|
159
|
-
continue;
|
|
160
52
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
issues.push(issue(
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
issues.push(issue(key, "legacy.root.forbidden", `${key} is not part of the clean envelope (set allowLegacyEnvelope to warn instead)`));
|
|
53
|
+
}
|
|
54
|
+
if (!isSnapshot) {
|
|
55
|
+
for (const bucket of Object.keys(KNOWN_OPEN_BUCKETS)) {
|
|
56
|
+
if (input[bucket] !== undefined) {
|
|
57
|
+
issues.push(issue(bucket, "SCOPE-2", `${bucket} SHOULD be omitted on non-snapshot content types unless opted in`));
|
|
169
58
|
}
|
|
170
|
-
continue;
|
|
171
59
|
}
|
|
172
|
-
issues.push(issue(key, "root.unknown", `unexpected root key "${key}"`, "warning"));
|
|
173
60
|
}
|
|
174
61
|
const hasError = issues.some((i) => i.severity === "error");
|
|
175
62
|
const hasWarning = issues.some((i) => i.severity === "warning");
|
|
176
63
|
const ok = options.strict ? !hasError && !hasWarning : !hasError;
|
|
177
|
-
return { ok, issues, recordId };
|
|
64
|
+
return { ok, issues, recordId: base.recordId };
|
|
178
65
|
}
|
|
179
|
-
/** Validate many records; returns per-record results plus aggregate ok. */
|
|
180
66
|
export function validateMemorixRecords(records, options = {}) {
|
|
181
67
|
const results = records.map((r) => validateMemorixRecord(r, options));
|
|
182
68
|
return { ok: results.every((r) => r.ok), results };
|
|
183
69
|
}
|
|
184
|
-
/** Parse JSON text that is either one object or an array / NDJSON stream. */
|
|
185
|
-
export function parseRecordsJson(text) {
|
|
186
|
-
const trimmed = text.trim();
|
|
187
|
-
if (!trimmed)
|
|
188
|
-
return [];
|
|
189
|
-
if (trimmed.startsWith("[")) {
|
|
190
|
-
const parsed = JSON.parse(trimmed);
|
|
191
|
-
if (!Array.isArray(parsed)) {
|
|
192
|
-
throw new Error("Expected a JSON array of records");
|
|
193
|
-
}
|
|
194
|
-
return parsed;
|
|
195
|
-
}
|
|
196
|
-
if (trimmed.startsWith("{")) {
|
|
197
|
-
// Prefer single object; if multiple JSON objects on separate lines, treat as NDJSON.
|
|
198
|
-
const lines = trimmed.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
199
|
-
if (lines.length > 1 && lines.every((l) => l.startsWith("{"))) {
|
|
200
|
-
return lines.map((line) => JSON.parse(line));
|
|
201
|
-
}
|
|
202
|
-
return [JSON.parse(trimmed)];
|
|
203
|
-
}
|
|
204
|
-
// NDJSON without leading [
|
|
205
|
-
return trimmed
|
|
206
|
-
.split(/\r?\n/)
|
|
207
|
-
.map((l) => l.trim())
|
|
208
|
-
.filter(Boolean)
|
|
209
|
-
.map((line) => JSON.parse(line));
|
|
210
|
-
}
|
|
211
70
|
//# sourceMappingURL=validate.js.map
|
package/dist/validate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Memorix Format — Requirements (product layer)
|
|
2
|
+
|
|
3
|
+
**Package:** `@x12i/memorix-format`
|
|
4
|
+
**Depends on:** `@x12i/memorix-base-format` (fixed skeleton — do not re-state ENV/ID/DATA/SYS here).
|
|
5
|
+
|
|
6
|
+
This document covers **Memorix product** open-bucket conventions. Base envelope MUST be satisfied first (`TOOL-3`: layer checks on base).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Known open buckets (when present)
|
|
11
|
+
|
|
12
|
+
**BUCKET-5.**
|
|
13
|
+
|
|
14
|
+
| Bucket | MUST contain | MUST NOT contain |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `analytics` | Numeric rollups | The underlying list of records |
|
|
17
|
+
| `enrichment` | External / prior-system values as received | Interpreted judgments |
|
|
18
|
+
| `insights` | Judgments, scores, recommendations | Anything a reader would mistake for a stored native fact |
|
|
19
|
+
|
|
20
|
+
These names are **Memorix declarations**, not base-reserved keys. Base classifies them as open buckets.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Associated properties
|
|
25
|
+
|
|
26
|
+
**ASSOC-1.** Keys matching Memorix association patterns (`associated…`, legacy short names, `associatedTop…`, `associated…Enrichment`, custom `associated` prefix).
|
|
27
|
+
|
|
28
|
+
**ASSOC-2.** When present, MUST be an array.
|
|
29
|
+
|
|
30
|
+
**ASSOC-3.** MUST be omitted when empty — never `[]`.
|
|
31
|
+
|
|
32
|
+
**ASSOC-5.** Items MUST be plain objects (linked payloads), not full envelopes.
|
|
33
|
+
|
|
34
|
+
**ASSOC-6.** Resolution MUST be snapshot-first (see association-format.md). Enforced by associator tooling, documented here.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Snapshot vs minimal (SCOPE)
|
|
39
|
+
|
|
40
|
+
**SCOPE-1.** Full open-bucket set SHOULD appear primarily on snapshots.
|
|
41
|
+
|
|
42
|
+
**SCOPE-2.** Non-snapshot content types MUST include `recordId` + `data`; SHOULD omit `analytics` / `insights` / most `associated*` unless opted in via ContentTypeMeta.
|
|
43
|
+
|
|
44
|
+
**SCOPE-3.** Readers MUST treat missing optional buckets as N/A, not error.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Validator
|
|
49
|
+
|
|
50
|
+
**TOOL-3.** `validateMemorixRecord` = `validateMemorixBaseRecord` + profile/ASSOC/SCOPE.
|
|
51
|
+
|
|
52
|
+
Built-in profiles: `snapshots`, `minimal` (`--profile=`).
|
package/docs/memorix-format.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Memorix Record Format
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **Package split (2.0):** Core envelope → [`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format).
|
|
4
|
+
> Product open buckets / associations → this package (`@x12i/memorix-format`).
|
|
5
|
+
> Requirements: [memorix-format-requirements.md](./memorix-format-requirements.md).
|
|
4
6
|
|
|
5
7
|
## Purpose
|
|
6
8
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_id": "6914a132880706529ece3da1",
|
|
3
2
|
"recordId": "69d216ed358778e93215787e",
|
|
4
3
|
"concept": {
|
|
5
4
|
"identifier": {
|
|
@@ -10,22 +9,14 @@
|
|
|
10
9
|
"title": "Oracle WebLogic Server RCE (CVE-2020-14882)"
|
|
11
10
|
},
|
|
12
11
|
"data": {
|
|
13
|
-
"name": "Oracle WebLogic Server RCE (CVE-2020-14882)",
|
|
14
12
|
"cveId": "CVE-2020-14882",
|
|
15
|
-
"severity": "critical"
|
|
16
|
-
"pluginId": 142594
|
|
13
|
+
"severity": "critical"
|
|
17
14
|
},
|
|
18
15
|
"associatedData": [
|
|
19
|
-
{ "assetIp": "10.100.24.31", "hostname": "BGAK105-002" }
|
|
20
|
-
{ "assetIp": "10.100.24.41", "hostname": "BGAK105-003" }
|
|
16
|
+
{ "assetIp": "10.100.24.31", "hostname": "BGAK105-002" }
|
|
21
17
|
],
|
|
22
18
|
"analytics": {
|
|
23
|
-
"affectedAssets": { "total":
|
|
24
|
-
},
|
|
25
|
-
"enrichment": {
|
|
26
|
-
"cvss": { "baseScore": 9.8 },
|
|
27
|
-
"epss": { "epss": 0.94454 },
|
|
28
|
-
"kev": { "isKnownExploited": true }
|
|
19
|
+
"affectedAssets": { "total": 1 }
|
|
29
20
|
},
|
|
30
21
|
"insights": {
|
|
31
22
|
"exploitability": { "level": "HIGH", "confidence": 0.92 }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaNote": "Associated* naming conventions + legacy short names. No objectType bindings.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"prefix": "associated",
|
|
5
|
+
"defaultTopMaxItems": 5,
|
|
6
|
+
"legacyNames": [
|
|
7
|
+
"associatedData",
|
|
8
|
+
"associatedDiscovery",
|
|
9
|
+
"associatedAnalysis",
|
|
10
|
+
"associatedSiblings",
|
|
11
|
+
"associatedChildren",
|
|
12
|
+
"associatedAssets",
|
|
13
|
+
"associatedSubnets"
|
|
14
|
+
],
|
|
15
|
+
"namingForms": [
|
|
16
|
+
{
|
|
17
|
+
"form": "content",
|
|
18
|
+
"pattern": "associated[Top?]<ObjectType><ContentType>",
|
|
19
|
+
"kind": "linked-payloads",
|
|
20
|
+
"shape": "array-of-objects"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"form": "enrichment",
|
|
24
|
+
"pattern": "associated[Top?]<ObjectType>Enrichment",
|
|
25
|
+
"kind": "linked-payloads",
|
|
26
|
+
"shape": "array-of-objects"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"form": "snapshotsRootProperty",
|
|
30
|
+
"pattern": "associated[Top?]<ObjectType>Snapshots<RootProperty>",
|
|
31
|
+
"kind": "linked-payloads",
|
|
32
|
+
"shape": "array-of-objects"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"form": "concepts",
|
|
36
|
+
"pattern": "associated[Top?]<ObjectType>Concepts",
|
|
37
|
+
"kind": "linked-payloads",
|
|
38
|
+
"shape": "array-of-objects"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"form": "recordIds",
|
|
42
|
+
"pattern": "associated[Top?]<ObjectType>RecordIds",
|
|
43
|
+
"kind": "linked-payloads",
|
|
44
|
+
"shape": "array-of-objects"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"form": "customDomain",
|
|
48
|
+
"pattern": "associated<CustomName>",
|
|
49
|
+
"kind": "linked-payloads",
|
|
50
|
+
"shape": "array-of-objects",
|
|
51
|
+
"examples": ["associatedTopology", "associatedCompliance"]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"rules": {
|
|
55
|
+
"ASSOC-2": "when present MUST be an array",
|
|
56
|
+
"ASSOC-3": "MUST be omitted when empty — never []",
|
|
57
|
+
"ASSOC-6": "resolution MUST be snapshot-first (match *-snapshots, then content by recordId)"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaNote": "Content-type profiles (policies). objectType is filled by the caller — never hardcoded here.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"profiles": {
|
|
5
|
+
"snapshots": {
|
|
6
|
+
"contentType": "snapshots",
|
|
7
|
+
"collectionPostfix": "snapshots",
|
|
8
|
+
"openBucketsRef": "open-buckets.json",
|
|
9
|
+
"policy": {
|
|
10
|
+
"unknownOpenBuckets": "allow",
|
|
11
|
+
"emptyBuckets": "reject"
|
|
12
|
+
},
|
|
13
|
+
"read": {
|
|
14
|
+
"missingOptionalIsNa": true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"minimal": {
|
|
18
|
+
"policy": {
|
|
19
|
+
"unknownOpenBuckets": "warn",
|
|
20
|
+
"emptyBuckets": "reject"
|
|
21
|
+
},
|
|
22
|
+
"read": {
|
|
23
|
+
"missingOptionalIsNa": true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaNote": "Content-type vocabulary used by Memorix. Not object types. Aligned with managed Catalox entity descriptors + product vocabulary.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"canonicalContentType": "snapshots",
|
|
5
|
+
"contentTypes": {
|
|
6
|
+
"snapshots": {
|
|
7
|
+
"profile": "snapshots",
|
|
8
|
+
"description": "Full readable object view — may carry known open buckets and associated*"
|
|
9
|
+
},
|
|
10
|
+
"discovery": {
|
|
11
|
+
"profile": "minimal",
|
|
12
|
+
"description": "Discovery / Q&A style content"
|
|
13
|
+
},
|
|
14
|
+
"analysis": {
|
|
15
|
+
"profile": "minimal",
|
|
16
|
+
"description": "Analysis content"
|
|
17
|
+
},
|
|
18
|
+
"decisions": {
|
|
19
|
+
"profile": "minimal",
|
|
20
|
+
"description": "Decision content",
|
|
21
|
+
"collectionPostfixVariants": ["decision", "decisions"]
|
|
22
|
+
},
|
|
23
|
+
"scoped": {
|
|
24
|
+
"profile": "minimal",
|
|
25
|
+
"description": "Scoped slice"
|
|
26
|
+
},
|
|
27
|
+
"legacy": {
|
|
28
|
+
"profile": "minimal",
|
|
29
|
+
"description": "Quarantined / legacy slice"
|
|
30
|
+
},
|
|
31
|
+
"narratives": {
|
|
32
|
+
"profile": "minimal",
|
|
33
|
+
"description": "Narrative records"
|
|
34
|
+
},
|
|
35
|
+
"narrative-steps": {
|
|
36
|
+
"profile": "minimal",
|
|
37
|
+
"description": "Narrative step records"
|
|
38
|
+
},
|
|
39
|
+
"workflow": {
|
|
40
|
+
"profile": "workflow",
|
|
41
|
+
"description": "Product workflow overlay (memorix-workflow/1) — not the snapshot envelope"
|
|
42
|
+
},
|
|
43
|
+
"extrated": {
|
|
44
|
+
"profile": "minimal",
|
|
45
|
+
"description": "Extracted document content (managed key spelling as in Catalox descriptors)"
|
|
46
|
+
},
|
|
47
|
+
"chunks": {
|
|
48
|
+
"profile": "minimal",
|
|
49
|
+
"description": "Document chunks"
|
|
50
|
+
},
|
|
51
|
+
"ai-processing": {
|
|
52
|
+
"profile": "minimal",
|
|
53
|
+
"description": "Product vocabulary — optional CT"
|
|
54
|
+
},
|
|
55
|
+
"context": {
|
|
56
|
+
"profile": "minimal",
|
|
57
|
+
"description": "Product vocabulary — optional CT"
|
|
58
|
+
},
|
|
59
|
+
"suggested-plans": {
|
|
60
|
+
"profile": "minimal",
|
|
61
|
+
"description": "Product vocabulary — optional CT"
|
|
62
|
+
},
|
|
63
|
+
"suggested-actions": {
|
|
64
|
+
"profile": "minimal",
|
|
65
|
+
"description": "Product vocabulary — optional CT"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaNote": "INVENTORY ONLY — derived from managed Catalox entity descriptors (memorix-retrieval/catalox-seeds). Not format law. Not agent packs. Used to keep format meta aligned with live managed objects. Regenerate when descriptors change.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"source": "memorix-retrieval/catalox-seeds/inputs/entity-descriptors/*.json",
|
|
5
|
+
"scannedAt": "2026-07-21",
|
|
6
|
+
"alsoScanned": [
|
|
7
|
+
{
|
|
8
|
+
"source": "ebooks/memorix-seed/entity-descriptors/*.json",
|
|
9
|
+
"note": "Demo domain — same CT/open-bucket conventions; no new format vocabulary"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"managedObjectTypes": [
|
|
13
|
+
"assets",
|
|
14
|
+
"content-documents",
|
|
15
|
+
"knowledge-playbooks",
|
|
16
|
+
"subnets",
|
|
17
|
+
"topology-zones",
|
|
18
|
+
"vulnerabilities",
|
|
19
|
+
"vulnerabilities-groups"
|
|
20
|
+
],
|
|
21
|
+
"contentTypesObserved": [
|
|
22
|
+
"analysis",
|
|
23
|
+
"chunks",
|
|
24
|
+
"decisions",
|
|
25
|
+
"discovery",
|
|
26
|
+
"extrated",
|
|
27
|
+
"legacy",
|
|
28
|
+
"narrative-steps",
|
|
29
|
+
"narratives",
|
|
30
|
+
"scoped",
|
|
31
|
+
"snapshots",
|
|
32
|
+
"workflow"
|
|
33
|
+
],
|
|
34
|
+
"contentTypeNotes": [
|
|
35
|
+
{
|
|
36
|
+
"contentType": "decisions",
|
|
37
|
+
"note": "Some descriptors use collection postfix 'decision' (singular) while the contentTypes key is 'decisions'"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"contentType": "extrated",
|
|
41
|
+
"note": "Spelling as managed in content-documents descriptor (key/postfix/collection)"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"associatedPropertiesObserved": [
|
|
45
|
+
"associatedAssetsSnapshots",
|
|
46
|
+
"associatedSubnetsAnalysis",
|
|
47
|
+
"associatedSubnetsDiscovery",
|
|
48
|
+
"associatedSubnetsSnapshots",
|
|
49
|
+
"associatedTopVulnerabilitiesAnalysis",
|
|
50
|
+
"associatedTopology",
|
|
51
|
+
"associatedTopologyZonesAnalysis",
|
|
52
|
+
"associatedTopologyZonesSnapshots",
|
|
53
|
+
"associatedVulnerabilitiesAnalysis",
|
|
54
|
+
"associatedVulnerabilitiesDecisions",
|
|
55
|
+
"associatedVulnerabilitiesDiscovery",
|
|
56
|
+
"associatedVulnerabilitiesGroupsAnalysis",
|
|
57
|
+
"associatedVulnerabilitiesGroupsDiscovery",
|
|
58
|
+
"associatedVulnerabilitiesGroupsEnrichment",
|
|
59
|
+
"associatedVulnerabilitiesGroupsSnapshots"
|
|
60
|
+
],
|
|
61
|
+
"customRootPropertiesObserved": [
|
|
62
|
+
{
|
|
63
|
+
"name": "possiblePaths",
|
|
64
|
+
"kind": "custom",
|
|
65
|
+
"shape": "object",
|
|
66
|
+
"objectTypes": ["subnets"],
|
|
67
|
+
"note": "Declared on subnets descriptors as managed rootProperty — open bucket, not base-reserved and not a product-known open bucket"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"platformOpenBucketsObservedInProductDocs": ["analytics", "enrichment", "insights"],
|
|
71
|
+
"alignmentChecks": {
|
|
72
|
+
"allContentTypesCoveredInContentTypesJson": true,
|
|
73
|
+
"platformOpenBucketsPresentInOpenBucketsJson": ["analytics", "enrichment", "insights"],
|
|
74
|
+
"associatedNamesFollowPrefixConvention": true,
|
|
75
|
+
"noObjectTypesInFormatLawJson": true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schemaNote": "Memorix product open buckets — content-type level, no objectType. Kind meanings for snapshots.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"openBuckets": {
|
|
5
|
+
"analytics": {
|
|
6
|
+
"kind": "rollup",
|
|
7
|
+
"shape": "object",
|
|
8
|
+
"title": "Analytics",
|
|
9
|
+
"description": "Numeric rollups about linked lists"
|
|
10
|
+
},
|
|
11
|
+
"enrichment": {
|
|
12
|
+
"kind": "sourced",
|
|
13
|
+
"shape": "object",
|
|
14
|
+
"title": "Enrichment",
|
|
15
|
+
"description": "External / prior-system sourced values as received"
|
|
16
|
+
},
|
|
17
|
+
"insights": {
|
|
18
|
+
"kind": "interpretive",
|
|
19
|
+
"shape": "object",
|
|
20
|
+
"title": "Insights",
|
|
21
|
+
"description": "Interpreted judgments, scores, recommendations"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|