@x12i/static-memorix-explorer-api 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.
@@ -0,0 +1,77 @@
1
+ import path from "node:path";
2
+ import { store } from "../storage/InMemoryStore.js";
3
+ import { CONTENT_TYPES } from "../types.js";
4
+ import { listJsonFiles, basenameNoExt, readJson } from "../storage/fs.js";
5
+ import { METADATA_DIR } from "../config.js";
6
+ import { isDir } from "../storage/fs.js";
7
+ import fs from "node:fs";
8
+ /**
9
+ * Discover narrative keys tagged on records via doc.narratives.*.
10
+ */
11
+ export function discoverRecordNarrativeTags(entityName) {
12
+ const tags = new Set();
13
+ for (const ct of CONTENT_TYPES) {
14
+ const records = store.getCollection(entityName, ct);
15
+ for (const doc of records) {
16
+ const narratives = doc.narratives || {};
17
+ for (const key of Object.keys(narratives))
18
+ tags.add(key);
19
+ }
20
+ }
21
+ return [...tags];
22
+ }
23
+ export function mergeCatalogWithSignals(authored, recordTags) {
24
+ const result = [];
25
+ const authoredKeys = Object.keys(authored || {});
26
+ for (const key of authoredKeys) {
27
+ result.push({ ...authored[key], key, signaled: recordTags.includes(key) });
28
+ }
29
+ for (const tag of recordTags) {
30
+ if (!authoredKeys.includes(tag)) {
31
+ result.push({ key: tag, signaled: true });
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ export function getMergedNarratives(entityName) {
37
+ if (entityName) {
38
+ const authored = store.getMeta(`narratives/${entityName}`) || {};
39
+ const recordTags = discoverRecordNarrativeTags(entityName);
40
+ return mergeCatalogWithSignals(authored, recordTags);
41
+ }
42
+ // global across all entities
43
+ const dir = path.join(METADATA_DIR, "narratives");
44
+ const out = {};
45
+ for (const file of listJsonFiles(dir)) {
46
+ const name = basenameNoExt(file);
47
+ const authored = readJson(file, {});
48
+ const recordTags = discoverRecordNarrativeTags(name);
49
+ out[name] = mergeCatalogWithSignals(authored, recordTags);
50
+ }
51
+ return out;
52
+ }
53
+ export function getRawNarratives(entityName) {
54
+ return (store.getMeta(`narratives/${entityName}`) || {});
55
+ }
56
+ export function queryNarrativeRecords(entityName, key) {
57
+ const results = [];
58
+ for (const ct of CONTENT_TYPES) {
59
+ const records = store.getCollection(entityName, ct);
60
+ for (const doc of records) {
61
+ const narratives = doc.narratives || {};
62
+ const val = narratives[key];
63
+ if (val === true || val === key)
64
+ results.push(doc);
65
+ }
66
+ }
67
+ return results;
68
+ }
69
+ export function listNarrativeEntities() {
70
+ const dir = path.join(METADATA_DIR, "narratives");
71
+ if (!isDir(dir))
72
+ return [];
73
+ return fs
74
+ .readdirSync(dir)
75
+ .filter((f) => f.endsWith(".json"))
76
+ .map((f) => basenameNoExt(f));
77
+ }
@@ -0,0 +1,71 @@
1
+ import { store } from "../storage/InMemoryStore.js";
2
+ import { CONTENT_TYPES } from "../types.js";
3
+ /**
4
+ * Full-scan all record arrays in ./data/{name}/*.json, compute property
5
+ * occurrence percentages, data types, and paths.
6
+ */
7
+ export function computeRootPropertyCatalog(objectType) {
8
+ const counts = new Map();
9
+ const typesMap = new Map();
10
+ const samples = new Map();
11
+ let totalDocs = 0;
12
+ for (const ct of CONTENT_TYPES) {
13
+ const records = store.getCollection(objectType, ct);
14
+ for (const doc of records) {
15
+ totalDocs++;
16
+ const flat = flatten(doc);
17
+ for (const [p, v] of Object.entries(flat)) {
18
+ counts.set(p, (counts.get(p) || 0) + 1);
19
+ const t = typeOf(v);
20
+ if (!typesMap.has(p))
21
+ typesMap.set(p, new Set());
22
+ typesMap.get(p).add(t);
23
+ if (!samples.has(p))
24
+ samples.set(p, []);
25
+ const arr = samples.get(p);
26
+ if (arr.length < 3 && !arr.includes(v))
27
+ arr.push(v);
28
+ }
29
+ }
30
+ }
31
+ const entries = [];
32
+ for (const [p, c] of counts.entries()) {
33
+ entries.push({
34
+ path: p,
35
+ occurrencePct: totalDocs ? Math.round((c / totalDocs) * 1000) / 10 : 0,
36
+ types: [...(typesMap.get(p) || [])],
37
+ sampleValues: samples.get(p) || [],
38
+ });
39
+ }
40
+ entries.sort((a, b) => b.occurrencePct - a.occurrencePct);
41
+ // Persist back to metadata
42
+ const meta = store.getMeta(`object-types/${objectType}`) || { name: objectType };
43
+ meta.rootPropertyCatalog = entries;
44
+ store.setMeta(`object-types/${objectType}`, meta);
45
+ return entries;
46
+ }
47
+ function flatten(obj, prefix = "") {
48
+ const out = {};
49
+ if (obj == null || typeof obj !== "object") {
50
+ if (prefix)
51
+ out[prefix] = obj;
52
+ return out;
53
+ }
54
+ for (const [k, v] of Object.entries(obj)) {
55
+ const key = prefix ? `${prefix}.${k}` : k;
56
+ if (v != null && typeof v === "object" && !Array.isArray(v)) {
57
+ Object.assign(out, flatten(v, key));
58
+ }
59
+ else {
60
+ out[key] = v;
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ function typeOf(v) {
66
+ if (v === null)
67
+ return "null";
68
+ if (Array.isArray(v))
69
+ return "array";
70
+ return typeof v;
71
+ }
@@ -0,0 +1,191 @@
1
+ import mingo from "mingo";
2
+ export function parseFilterToken(token) {
3
+ const idx1 = token.indexOf(":");
4
+ if (idx1 === -1)
5
+ return null;
6
+ const field = token.slice(0, idx1);
7
+ const rest = token.slice(idx1 + 1);
8
+ const idx2 = rest.indexOf(":");
9
+ if (idx2 === -1) {
10
+ // treat as eq
11
+ return { field, op: "eq", value: coerce(rest), raw: token };
12
+ }
13
+ const op = rest.slice(0, idx2);
14
+ const value = rest.slice(idx2 + 1);
15
+ // Narrative virtual filter interception: narrativeId:eq:has-vulns /
16
+ // narrativeKey:eq:has-vulns -> evaluate doc.narratives['has-vulns'].
17
+ if (field === "narrativeId" || field === "narrativeKey") {
18
+ return {
19
+ field,
20
+ op: op || "eq",
21
+ value: coerceForOp(op || "eq", value),
22
+ raw: token,
23
+ isVirtual: true,
24
+ };
25
+ }
26
+ if (!["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "exists", "regex"].includes(op)) {
27
+ return { field, op: "eq", value: coerce(rest), raw: token };
28
+ }
29
+ return { field, op, value: coerceForOp(op, value), raw: token };
30
+ }
31
+ export function coerce(value) {
32
+ if (value === "true")
33
+ return true;
34
+ if (value === "false")
35
+ return false;
36
+ if (value === "null")
37
+ return null;
38
+ if (value !== "" && !isNaN(Number(value)))
39
+ return Number(value);
40
+ return value;
41
+ }
42
+ function coerceForOp(op, value) {
43
+ if (op === "in" || op === "nin") {
44
+ return value
45
+ .split("|")
46
+ .map((v) => coerce(v.trim()))
47
+ .filter((v) => v !== "");
48
+ }
49
+ if (op === "exists") {
50
+ return !(value === "false" || value === "0");
51
+ }
52
+ if (op === "regex") {
53
+ return value;
54
+ }
55
+ return coerce(value);
56
+ }
57
+ /**
58
+ * Parse the filter param. Supports repeated values (array) and `;` separators.
59
+ * Returns virtual narrative filters separately so the caller can rewrite them.
60
+ */
61
+ export function parseExplorerFilters(filter) {
62
+ const tokens = toArray(filter)
63
+ .flatMap((f) => f.split(";"))
64
+ .map((f) => f.trim())
65
+ .filter(Boolean);
66
+ const criteria = {};
67
+ const virtual = [];
68
+ for (const token of tokens) {
69
+ const parsed = parseFilterToken(token);
70
+ if (!parsed)
71
+ continue;
72
+ if (parsed.isVirtual) {
73
+ virtual.push(parsed);
74
+ continue;
75
+ }
76
+ applyFilterToCriteria(criteria, parsed);
77
+ }
78
+ return { criteria, virtual };
79
+ }
80
+ function applyFilterToCriteria(criteria, parsed) {
81
+ const { field, op, value } = parsed;
82
+ switch (op) {
83
+ case "eq":
84
+ criteria[field] = value;
85
+ break;
86
+ case "ne":
87
+ criteria[field] = { $ne: value };
88
+ break;
89
+ case "gt":
90
+ criteria[field] = { $gt: value };
91
+ break;
92
+ case "gte":
93
+ criteria[field] = { $gte: value };
94
+ break;
95
+ case "lt":
96
+ criteria[field] = { $lt: value };
97
+ break;
98
+ case "lte":
99
+ criteria[field] = { $lte: value };
100
+ break;
101
+ case "in":
102
+ criteria[field] = { $in: value };
103
+ break;
104
+ case "nin":
105
+ criteria[field] = { $nin: value };
106
+ break;
107
+ case "exists":
108
+ criteria[field] = { $exists: value };
109
+ break;
110
+ case "regex":
111
+ criteria[field] = { $regex: value, $options: "i" };
112
+ break;
113
+ }
114
+ }
115
+ export function parseSortParams(sort) {
116
+ const tokens = toArray(sort)
117
+ .flatMap((s) => s.split(";"))
118
+ .map((s) => s.trim())
119
+ .filter(Boolean);
120
+ const sortSpec = {};
121
+ for (const token of tokens) {
122
+ const [field, dirRaw] = token.split(":");
123
+ const dir = (dirRaw || "asc").toLowerCase();
124
+ sortSpec[field] = dir === "desc" ? -1 : 1;
125
+ }
126
+ return sortSpec;
127
+ }
128
+ function toArray(v) {
129
+ if (v === undefined || v === null)
130
+ return [];
131
+ return Array.isArray(v) ? v : [v];
132
+ }
133
+ /**
134
+ * Resolve virtual narrative filters into a predicate that checks
135
+ * doc.narratives[key] presence / truthiness.
136
+ */
137
+ export function applyVirtualNarrativeFilters(records, virtual) {
138
+ if (virtual.length === 0)
139
+ return records;
140
+ return records.filter((doc) => {
141
+ const narratives = doc.narratives || {};
142
+ return virtual.every((vf) => {
143
+ const key = String(vf.value);
144
+ const actual = narratives[key];
145
+ if (vf.op === "exists")
146
+ return vf.value ? actual !== undefined : actual === undefined;
147
+ if (vf.op === "eq")
148
+ return actual === true || actual === vf.value;
149
+ return true;
150
+ });
151
+ });
152
+ }
153
+ /**
154
+ * Execute a query against an in-memory record array using mingo, with
155
+ * full-text search, virtual narrative filters, sorting, and pagination.
156
+ */
157
+ export function queryMockCollection(records, queryParams) {
158
+ let pipeline = [...records];
159
+ // 1. Intercept narrative virtual filters
160
+ const { criteria, virtual } = parseExplorerFilters(queryParams.filter);
161
+ pipeline = applyVirtualNarrativeFilters(pipeline, virtual);
162
+ // 2. Full-Text Search
163
+ const term = (queryParams.searchText || queryParams.q || "").toLowerCase();
164
+ if (term) {
165
+ pipeline = pipeline.filter((doc) => JSON.stringify(doc).toLowerCase().includes(term));
166
+ }
167
+ // 3. Mingo Mongo Query Execution
168
+ const query = new mingo.Query(criteria);
169
+ let cursor = query.find(pipeline);
170
+ // 4. Sorting
171
+ const sortSpec = parseSortParams(queryParams.sort);
172
+ if (Object.keys(sortSpec).length > 0) {
173
+ cursor = cursor.sort(sortSpec);
174
+ }
175
+ // Materialize all matched docs (mingo cursors are single-use, so we cannot
176
+ // call .count() and then .all() on the same cursor).
177
+ const matched = cursor.all();
178
+ const total = matched.length;
179
+ // 5. Pagination
180
+ const limit = Math.min(Number(queryParams.limit) || 50, 500);
181
+ const offset = Number(queryParams.offset) || Number(queryParams.skip) || 0;
182
+ const rows = matched.slice(offset, offset + limit);
183
+ return {
184
+ rows,
185
+ page: {
186
+ offset,
187
+ limit,
188
+ total: queryParams.includeTotal ? total : undefined,
189
+ },
190
+ };
191
+ }
@@ -0,0 +1,152 @@
1
+ import { store } from "../storage/InMemoryStore.js";
2
+ import { CONTENT_TYPES, } from "../types.js";
3
+ import { queryMockCollection } from "../engine/query.js";
4
+ import { executeListDescriptor, getListDescriptor } from "../engine/lists.js";
5
+ import { findByIdentity } from "../engine/identity.js";
6
+ export function inferIdField(entityName, ct) {
7
+ if (ct === "memory")
8
+ return "memoryId";
9
+ const sample = store.getCollection(entityName, ct)[0];
10
+ if (sample) {
11
+ if ("recordId" in sample)
12
+ return "recordId";
13
+ if ("entityId" in sample)
14
+ return "entityId";
15
+ if ("id" in sample)
16
+ return "id";
17
+ }
18
+ return "recordId";
19
+ }
20
+ /**
21
+ * Find a record within a collection using an identity map (recordId, entityId,
22
+ * eventId, knowledgeId, or memoryId). At least one identity key is required.
23
+ */
24
+ export function findRecordByIdentity(entityName, ct, identity) {
25
+ const records = store.getCollection(entityName, ct);
26
+ return findByIdentity(records, identity);
27
+ }
28
+ export function findRecord(entityName, ct, recordId) {
29
+ const idField = inferIdField(entityName, ct);
30
+ return store
31
+ .getCollection(entityName, ct)
32
+ .find((d) => String(d[idField]) === String(recordId));
33
+ }
34
+ export async function getRecordsCollection(params) {
35
+ const target = params.target;
36
+ // The `memory` target routes to the memory tier regardless of entityName.
37
+ if (target === "memory") {
38
+ const ct = "memory";
39
+ const records = store.getCollection("memory", ct);
40
+ return queryMockCollection(records, params);
41
+ }
42
+ const entityName = params.entityName;
43
+ const mode = params.mode || "auto";
44
+ const listId = params.listId;
45
+ if (mode === "descriptor" || (mode === "auto" && listId)) {
46
+ const resolvedListId = listId || `${entityName}-default`;
47
+ const listSpec = getListDescriptor(resolvedListId);
48
+ if (listSpec) {
49
+ return executeListDescriptor(listSpec, params);
50
+ }
51
+ // fall through to raw if descriptor missing
52
+ }
53
+ const ct = params.contentType || "snapshots";
54
+ const records = store.getCollection(entityName, ct);
55
+ return queryMockCollection(records, params);
56
+ }
57
+ export function getRecordsFull(entityName, recordId) {
58
+ const identity = { recordId };
59
+ const snapshot = findRecordByIdentity(entityName, "snapshots", identity);
60
+ const analysis = findRecordByIdentity(entityName, "analysis", identity);
61
+ const decisions = findRecordByIdentity(entityName, "decisions", identity);
62
+ const memory = findRecordByIdentity(entityName, "memory", identity);
63
+ return {
64
+ recordId,
65
+ entityName,
66
+ contentTypes: { snapshot, analysis, decisions, memory },
67
+ };
68
+ }
69
+ export function getRecordsItem(entityName, recordId, itemDescriptor) {
70
+ const record = findRecordByIdentity(entityName, "snapshots", { recordId });
71
+ if (!record)
72
+ return undefined;
73
+ if (!itemDescriptor)
74
+ return record;
75
+ // Hydrate sections/fields from metadata/items
76
+ const sections = itemDescriptor.sections || [];
77
+ const out = { recordId, entityName };
78
+ for (const section of sections) {
79
+ const fields = section.fields || [];
80
+ out[section.key || section.id] = fields.map((f) => ({
81
+ field: f.field || f.key,
82
+ value: getPath(record, f.field || f.key),
83
+ label: f.label,
84
+ }));
85
+ }
86
+ return out;
87
+ }
88
+ /**
89
+ * Fetch a single content record by id. Supports the memory tier via memoryId.
90
+ * If entityName is empty and contentType is "memory", defaults to "memory".
91
+ * Otherwise, searches across all object types in the store.
92
+ */
93
+ export function getRecordContent(entityName, contentType, identity) {
94
+ if (contentType === "memory") {
95
+ entityName = entityName || "memory";
96
+ return findRecordByIdentity(entityName, contentType, identity);
97
+ }
98
+ if (!entityName) {
99
+ // Search across all collections for the identity key
100
+ const keys = store.getCollectionKeys();
101
+ for (const key of keys) {
102
+ const [ot, ct] = key.split("/");
103
+ if (ct !== contentType)
104
+ continue;
105
+ const found = findRecordByIdentity(ot, contentType, identity);
106
+ if (found)
107
+ return found;
108
+ }
109
+ return undefined;
110
+ }
111
+ return findRecordByIdentity(entityName, contentType, identity);
112
+ }
113
+ export function getRawCollection(entityName, collectionName) {
114
+ // memory tier is keyed by a standalone `memory` object type
115
+ if (collectionName === "memory" || entityName === "memory") {
116
+ return store.getCollection("memory", "memory");
117
+ }
118
+ const ct = collectionName;
119
+ if (CONTENT_TYPES.includes(ct)) {
120
+ return store.getCollection(entityName, ct);
121
+ }
122
+ // allow objectType/contentType form
123
+ const [ot, c] = collectionName.split("/");
124
+ if (CONTENT_TYPES.includes(c)) {
125
+ return store.getCollection(ot, c);
126
+ }
127
+ return [];
128
+ }
129
+ export function getRawItem(collectionName, recordId) {
130
+ if (collectionName === "memory") {
131
+ return findRecordByIdentity("memory", "memory", { memoryId: recordId });
132
+ }
133
+ const ct = collectionName;
134
+ if (CONTENT_TYPES.includes(ct)) {
135
+ return findRecordByIdentity("memory", ct, { recordId }) ??
136
+ findRecord("__any__", ct, recordId);
137
+ }
138
+ const [entityName, c] = collectionName.split("/");
139
+ return findRecord(entityName, c, recordId);
140
+ }
141
+ export async function getWorkspaceRecords(params) {
142
+ // cross-entity query using a workspace list descriptor
143
+ const listId = params.listId || "workspace-default";
144
+ const listSpec = getListDescriptor(listId);
145
+ if (!listSpec) {
146
+ return { rows: [], page: { offset: 0, limit: 50 } };
147
+ }
148
+ return executeListDescriptor(listSpec, params);
149
+ }
150
+ function getPath(obj, path) {
151
+ return path.split(".").reduce((acc, k) => (acc == null ? undefined : acc[k]), obj);
152
+ }
@@ -0,0 +1,110 @@
1
+ import { store } from "../storage/InMemoryStore.js";
2
+ import { buildAssociatedBucket, aliasClientToStorage, resolveInclude, } from "../engine/aliases.js";
3
+ import { inferIdField } from "../engine/records.js";
4
+ export function getSnapshotRaw(objectType, recordId) {
5
+ const idField = inferIdField(objectType, "snapshots");
6
+ const rawDoc = store
7
+ .getCollection(objectType, "snapshots")
8
+ .find((d) => String(d[idField]) === String(recordId));
9
+ if (!rawDoc)
10
+ return undefined;
11
+ return { rawDoc, idField };
12
+ }
13
+ export function getSnapshotNormalized(objectType, recordId, include) {
14
+ const found = getSnapshotRaw(objectType, recordId);
15
+ if (!found)
16
+ return undefined;
17
+ const { rawDoc } = found;
18
+ const { associated, strippedDoc } = buildAssociatedBucket(rawDoc);
19
+ const flags = resolveInclude(include);
20
+ const out = { ...strippedDoc, associated };
21
+ if (flags) {
22
+ for (const flag of flags) {
23
+ applyIncludeFlag(out, rawDoc, associated, flag);
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ function applyIncludeFlag(out, rawDoc, associated, flag) {
29
+ switch (flag) {
30
+ case "data":
31
+ out.data = associated.data ?? rawDoc.associatedData ?? [];
32
+ break;
33
+ case "discovery":
34
+ out.discovery =
35
+ associated.discovery ?? rawDoc.associatedInferred ?? rawDoc.associatedDiscovery ?? [];
36
+ break;
37
+ case "analysis":
38
+ out.analysis = associated.analysis ?? rawDoc.associatedAnalysis ?? [];
39
+ break;
40
+ case "enrichment":
41
+ out.enrichment = rawDoc.enrichment ?? [];
42
+ break;
43
+ case "insights":
44
+ out.insights = rawDoc.insights ?? [];
45
+ break;
46
+ case "system":
47
+ out.system = rawDoc.system ?? {};
48
+ break;
49
+ case "associated":
50
+ out.associated = associated;
51
+ break;
52
+ }
53
+ }
54
+ export function getSnapshotAssociated(objectType, recordId) {
55
+ const found = getSnapshotRaw(objectType, recordId);
56
+ if (!found)
57
+ return undefined;
58
+ const { associated, properties } = buildAssociatedBucket(found.rawDoc);
59
+ return { associated, properties };
60
+ }
61
+ export function getSnapshotAssociatedProperty(objectType, recordId, propertyName) {
62
+ const found = getSnapshotRaw(objectType, recordId);
63
+ if (!found)
64
+ return undefined;
65
+ const { associated } = buildAssociatedBucket(found.rawDoc);
66
+ if (propertyName in associated) {
67
+ return associated[propertyName];
68
+ }
69
+ // try alias resolution
70
+ const { fallbacks } = aliasClientToStorage(propertyName);
71
+ for (const fb of fallbacks) {
72
+ if (fb in found.rawDoc)
73
+ return found.rawDoc[fb];
74
+ }
75
+ return undefined;
76
+ }
77
+ export function getAssociatedProperties(objectType) {
78
+ const records = store.getCollection(objectType, "snapshots");
79
+ const agg = new Map();
80
+ for (const doc of records) {
81
+ for (const k of Object.keys(doc)) {
82
+ if (k.startsWith("associated")) {
83
+ const existing = agg.get(k) || { source: k, count: 0 };
84
+ const v = doc[k];
85
+ existing.count += Array.isArray(v) ? v.length : v != null ? 1 : 0;
86
+ agg.set(k, existing);
87
+ }
88
+ }
89
+ }
90
+ return [...agg.entries()].map(([source, v]) => ({
91
+ propertyName: source,
92
+ source,
93
+ count: v.count,
94
+ }));
95
+ }
96
+ /**
97
+ * Deterministic fingerprint for association plan/apply/verify.
98
+ */
99
+ export function computeAssociationFingerprint(objectType) {
100
+ const records = store.getCollection(objectType, "snapshots");
101
+ const serialized = JSON.stringify(records.map((r) => ({ id: r.id ?? r.recordId, keys: Object.keys(r) })));
102
+ return `fp:${objectType}:${hashString(serialized)}`;
103
+ }
104
+ export function hashString(s) {
105
+ let h = 5381;
106
+ for (let i = 0; i < s.length; i++) {
107
+ h = (h * 33) ^ s.charCodeAt(i);
108
+ }
109
+ return (h >>> 0).toString(16);
110
+ }