@x12i/static-memorix-explorer-api 1.2.0 → 1.3.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/README.md CHANGED
@@ -207,6 +207,28 @@ These strings are derived from `MEMORIX_ORG_ID`, `MEMORIX_AGENT_ID`, and the
207
207
  `MEMORIX_DEPLOYMENT_PROFILE` (which, when set to `ebook`, forces both IDs to
208
208
  `ebooks`).
209
209
 
210
+ ### File-backed routing
211
+
212
+ Those routed database names also select JSON files. The separator is `--`:
213
+
214
+ ```text
215
+ mocks/data/product/neo-memorix-entities--snapshots.json
216
+ mocks/data/product/neo-memorix-events--events.json
217
+ mocks/data/memory/neo-agent-memorix-catalox--memory.json
218
+ mocks/metadata/lists/neo-agent-memorix-catalox--my-plate.json
219
+ ```
220
+
221
+ For `MEMORIX_ORG_ID=neo` and `MEMORIX_AGENT_ID=neo-agent`, entity-like
222
+ collections use the first prefix, events use the second, and memory plus
223
+ metadata use the Catalox prefix. If a routed file does not exist, the server
224
+ loads its unprefixed counterpart as seed data, but all mutations are written
225
+ to the routed filename. This provides database-style isolation without a
226
+ database and preserves existing fixtures.
227
+
228
+ The health response exposes the resolved values under `fileRouting`.
229
+ See [guides/managing-json-files.md](./guides/managing-json-files.md) for the
230
+ complete precedence and migration rules.
231
+
210
232
  ## Feature Flag Enforcement
211
233
 
212
234
  The mock server enforces the following feature-flag gates with error responses:
@@ -247,6 +269,15 @@ and `knowledgeId`.
247
269
 
248
270
  ## Release history
249
271
 
272
+ ### v1.3.0
273
+ - Added database-routing parity through deterministic JSON filename prefixes.
274
+ - Added separate entity, event, and Catalox/metadata prefix resolution.
275
+ - Added migration-safe fallback from missing routed files to unprefixed seeds;
276
+ writes always persist to the selected routed file.
277
+ - Added resolved `fileRouting` prefixes to the health response.
278
+ - Updated metadata discovery to ignore other routed tenants.
279
+ - Added routed persistence and prefix regression coverage.
280
+
250
281
  ### v1.2.0
251
282
  - Added native `--port` and `--host` CLI options.
252
283
  - Added the canonical `static-memorix-explorer-api` executable while retaining
package/dist/config.d.ts CHANGED
@@ -18,6 +18,14 @@ export declare function buildMemorixDbString(): string;
18
18
  * e.g. "ebooks-memorix-catalox"
19
19
  */
20
20
  export declare function buildCataloxDbString(): string;
21
+ /**
22
+ * Files emulate routed databases by using the resolved database name as a
23
+ * prefix. The `--` separator is reserved for routing and must not be used in
24
+ * an unprefixed fixture basename.
25
+ */
26
+ export declare function buildDataFilePrefix(contentType: string): string;
27
+ export declare function buildMetadataFilePrefix(): string;
28
+ export declare function prefixedJsonFilename(prefix: string, basename: string): string;
21
29
  export declare const FLAGS: {
22
30
  METADATA_WRITES: boolean;
23
31
  PIPELINE_WRITES: boolean;
package/dist/config.js CHANGED
@@ -46,6 +46,24 @@ export function buildMemorixDbString() {
46
46
  export function buildCataloxDbString() {
47
47
  return `${MEMORIX_AGENT_ID}-memorix-catalox`;
48
48
  }
49
+ /**
50
+ * Files emulate routed databases by using the resolved database name as a
51
+ * prefix. The `--` separator is reserved for routing and must not be used in
52
+ * an unprefixed fixture basename.
53
+ */
54
+ export function buildDataFilePrefix(contentType) {
55
+ if (contentType === "events")
56
+ return `${MEMORIX_ORG_ID}-memorix-events`;
57
+ if (contentType === "memory")
58
+ return buildCataloxDbString();
59
+ return `${MEMORIX_ORG_ID}-memorix-entities`;
60
+ }
61
+ export function buildMetadataFilePrefix() {
62
+ return buildCataloxDbString();
63
+ }
64
+ export function prefixedJsonFilename(prefix, basename) {
65
+ return `${prefix}--${basename}.json`;
66
+ }
49
67
  // ---------------------------------------------------------------------------
50
68
  // Feature Flag Enforcement
51
69
  // ---------------------------------------------------------------------------
@@ -1,13 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { store } from "../storage/InMemoryStore.js";
3
3
  import { CONTENT_TYPES } from "../types.js";
4
- import { listJsonFiles, basenameNoExt } from "../storage/fs.js";
5
- import { METADATA_DIR, DATA_DIR } from "../config.js";
4
+ import { DATA_DIR } from "../config.js";
6
5
  import fs from "node:fs";
7
6
  import { isDir } from "../storage/fs.js";
8
7
  export function listObjectTypes() {
9
- const dir = path.join(METADATA_DIR, "object-types");
10
- return listJsonFiles(dir).map((f) => basenameNoExt(f));
8
+ return store.getMetaKeys()
9
+ .filter((key) => key.startsWith("object-types/"))
10
+ .map((key) => key.slice("object-types/".length));
11
11
  }
12
12
  export function listDataObjectTypes() {
13
13
  if (!isDir(DATA_DIR))
@@ -11,7 +11,7 @@ export declare function executeListDescriptor(desc: ListDescriptor, params?: Exp
11
11
  export declare function suggestListExtensions(entityName: string): {
12
12
  entityName: string;
13
13
  suggestions: {
14
- contentType: "analysis" | "decisions" | "events" | "memory";
14
+ contentType: "events" | "memory" | "analysis" | "decisions";
15
15
  collection: string;
16
16
  suggestion: string;
17
17
  }[];
@@ -1,14 +1,10 @@
1
- import path from "node:path";
2
1
  import { store } from "../storage/InMemoryStore.js";
3
2
  import { CONTENT_TYPES } from "../types.js";
4
- import { listJsonFiles, basenameNoExt } from "../storage/fs.js";
5
- import { METADATA_DIR } from "../config.js";
6
3
  import { queryMockCollection } from "../engine/query.js";
7
4
  export function listListDescriptors(entityName) {
8
- const dir = path.join(METADATA_DIR, "lists");
9
5
  const out = [];
10
- for (const file of listJsonFiles(dir)) {
11
- const desc = store.getMeta(`lists/${basenameNoExt(file)}`);
6
+ for (const key of store.getMetaKeys().filter((item) => item.startsWith("lists/"))) {
7
+ const desc = store.getMeta(key);
12
8
  if (desc && (!entityName || desc.entity === entityName || desc.entityName === entityName)) {
13
9
  out.push(desc);
14
10
  }
@@ -1,10 +1,5 @@
1
- import path from "node:path";
2
1
  import { store } from "../storage/InMemoryStore.js";
3
2
  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
3
  /**
9
4
  * Discover narrative keys tagged on records via doc.narratives.*.
10
5
  */
@@ -40,11 +35,10 @@ export function getMergedNarratives(entityName) {
40
35
  return mergeCatalogWithSignals(authored, recordTags);
41
36
  }
42
37
  // global across all entities
43
- const dir = path.join(METADATA_DIR, "narratives");
44
38
  const out = {};
45
- for (const file of listJsonFiles(dir)) {
46
- const name = basenameNoExt(file);
47
- const authored = readJson(file, {});
39
+ for (const metaKey of store.getMetaKeys().filter((key) => key.startsWith("narratives/"))) {
40
+ const name = metaKey.slice("narratives/".length);
41
+ const authored = store.getMeta(metaKey) || {};
48
42
  const recordTags = discoverRecordNarrativeTags(name);
49
43
  out[name] = mergeCatalogWithSignals(authored, recordTags);
50
44
  }
@@ -67,11 +61,7 @@ export function queryNarrativeRecords(entityName, key) {
67
61
  return results;
68
62
  }
69
63
  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));
64
+ return store.getMetaKeys()
65
+ .filter((key) => key.startsWith("narratives/"))
66
+ .map((key) => key.slice("narratives/".length));
77
67
  }
@@ -1,7 +1,4 @@
1
- import path from "node:path";
2
1
  import { store } from "../storage/InMemoryStore.js";
3
- import { basenameNoExt, listJsonFiles } from "../storage/fs.js";
4
- import { METADATA_DIR } from "../config.js";
5
2
  export function loadWriteDescriptor(writeDescriptorId) {
6
3
  const desc = store.getMeta(`write-descriptors/${writeDescriptorId}`);
7
4
  if (!desc) {
@@ -156,10 +153,9 @@ function getNextId(collection, idField = "recordId") {
156
153
  return () => String(counter++);
157
154
  }
158
155
  export function listWriteDescriptors() {
159
- const dir = path.join(METADATA_DIR, "write-descriptors");
160
156
  const out = [];
161
- for (const file of listJsonFiles(dir)) {
162
- const desc = store.getMeta(`write-descriptors/${basenameNoExt(file)}`);
157
+ for (const key of store.getMetaKeys().filter((item) => item.startsWith("write-descriptors/"))) {
158
+ const desc = store.getMeta(key);
163
159
  if (desc)
164
160
  out.push(desc);
165
161
  }
@@ -1,5 +1,5 @@
1
1
  import { store } from "../storage/InMemoryStore.js";
2
- import { buildMemorixDbString, buildCataloxDbString, metadataWritesEnabled, } from "../config.js";
2
+ import { buildMemorixDbString, buildCataloxDbString, buildDataFilePrefix, buildMetadataFilePrefix, metadataWritesEnabled, } from "../config.js";
3
3
  import { listObjectTypes } from "../engine/inventory.js";
4
4
  import { computeInventory, computeInventorySummary, computeInventoryIssues, computeGraph, } from "../engine/inventory.js";
5
5
  import { qp, str } from "./helpers.js";
@@ -25,6 +25,12 @@ export async function registerRoutes(app) {
25
25
  discoverySample: objectTypes,
26
26
  memorixDb: buildMemorixDbString(),
27
27
  cataloxDb: buildCataloxDbString(),
28
+ fileRouting: {
29
+ entitiesPrefix: buildDataFilePrefix("snapshots"),
30
+ eventsPrefix: buildDataFilePrefix("events"),
31
+ memoryPrefix: buildDataFilePrefix("memory"),
32
+ metadataPrefix: buildMetadataFilePrefix(),
33
+ },
28
34
  };
29
35
  if (includeInventory) {
30
36
  const rows = computeInventory({ sourceLens: "catalox-first" });
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { CONTENT_TYPES } from "../types.js";
4
- import { MOCKS_DIR, METADATA_DIR, DATA_DIR } from "../config.js";
4
+ import { MOCKS_DIR, METADATA_DIR, DATA_DIR, buildDataFilePrefix, buildMetadataFilePrefix, prefixedJsonFilename, } from "../config.js";
5
5
  import { validateRecordArray } from "../engine/identity.js";
6
6
  import { isDir, listJsonFiles, readJson, writeJson, basenameNoExt, } from "./fs.js";
7
7
  import { DISK_FLUSH_DEBOUNCE_MS } from "../config.js";
@@ -28,18 +28,33 @@ export class InMemoryStore {
28
28
  "write-descriptors": "write-descriptors",
29
29
  narratives: "narratives",
30
30
  };
31
+ const prefix = buildMetadataFilePrefix();
31
32
  for (const [subdir] of Object.entries(sections)) {
32
33
  const dir = path.join(METADATA_DIR, subdir);
33
- for (const file of listJsonFiles(dir)) {
34
- const key = `${subdir}/${basenameNoExt(file)}`;
35
- this.metadata.set(key, { path: file, data: readJson(file, {}) });
34
+ const files = listJsonFiles(dir);
35
+ const logicalNames = new Set();
36
+ for (const file of files) {
37
+ const name = basenameNoExt(file);
38
+ if (!name.includes("--"))
39
+ logicalNames.add(name);
40
+ if (name.startsWith(`${prefix}--`))
41
+ logicalNames.add(name.slice(prefix.length + 2));
42
+ }
43
+ for (const name of logicalNames) {
44
+ const baseFile = path.join(dir, `${name}.json`);
45
+ const routedFile = path.join(dir, prefixedJsonFilename(prefix, name));
46
+ const source = fs.existsSync(routedFile) ? routedFile : baseFile;
47
+ const key = `${subdir}/${name}`;
48
+ this.metadata.set(key, { path: routedFile, data: readJson(source, {}) });
36
49
  }
37
50
  }
38
- const agentsFile = path.join(METADATA_DIR, "agents.json");
39
- if (fs.existsSync(agentsFile)) {
51
+ const baseAgentsFile = path.join(METADATA_DIR, "agents.json");
52
+ const routedAgentsFile = path.join(METADATA_DIR, prefixedJsonFilename(prefix, "agents"));
53
+ const agentsSource = fs.existsSync(routedAgentsFile) ? routedAgentsFile : baseAgentsFile;
54
+ if (fs.existsSync(agentsSource)) {
40
55
  this.metadata.set("agents", {
41
- path: agentsFile,
42
- data: readJson(agentsFile, { agents: [] }),
56
+ path: routedAgentsFile,
57
+ data: readJson(agentsSource, { agents: [] }),
43
58
  });
44
59
  }
45
60
  }
@@ -51,9 +66,11 @@ export class InMemoryStore {
51
66
  if (!isDir(otDir))
52
67
  continue;
53
68
  for (const ct of CONTENT_TYPES) {
54
- const file = path.join(otDir, `${ct}.json`);
69
+ const baseFile = path.join(otDir, `${ct}.json`);
70
+ const file = path.join(otDir, prefixedJsonFilename(buildDataFilePrefix(ct), ct));
55
71
  const key = `${objectType}/${ct}`;
56
- const data = readJson(file, []);
72
+ const source = fs.existsSync(file) ? file : baseFile;
73
+ const data = readJson(source, []);
57
74
  // Startup validation: ensure records carry exactly one identity key
58
75
  // (MemorixRecordArraySchema mock). Memory collections use memoryId.
59
76
  const result = validateRecordArray(objectType, ct, data);
@@ -68,11 +85,13 @@ export class InMemoryStore {
68
85
  });
69
86
  }
70
87
  // system overrides
71
- const invFile = path.join(DATA_DIR, "system", "inventory.json");
72
- if (objectType === "system" && fs.existsSync(invFile)) {
88
+ const baseInvFile = path.join(DATA_DIR, "system", "inventory.json");
89
+ const invFile = path.join(DATA_DIR, "system", prefixedJsonFilename(buildDataFilePrefix("snapshots"), "inventory"));
90
+ const invSource = fs.existsSync(invFile) ? invFile : baseInvFile;
91
+ if (objectType === "system" && fs.existsSync(invSource)) {
73
92
  this.collections.set("system/inventory", {
74
93
  path: invFile,
75
- data: readJson(invFile, []),
94
+ data: readJson(invSource, []),
76
95
  });
77
96
  }
78
97
  }
@@ -89,7 +108,7 @@ export class InMemoryStore {
89
108
  }
90
109
  else {
91
110
  this.metadata.set(key, {
92
- path: path.join(METADATA_DIR, `${key}.json`),
111
+ path: path.join(METADATA_DIR, `${path.dirname(key)}/${prefixedJsonFilename(buildMetadataFilePrefix(), path.basename(key))}`),
93
112
  data,
94
113
  [DIRTY]: true,
95
114
  });
@@ -106,7 +125,7 @@ export class InMemoryStore {
106
125
  const c = this.collections.get(key);
107
126
  if (!c) {
108
127
  const newC = {
109
- path: path.join(DATA_DIR, objectType, `${ct}.json`),
128
+ path: path.join(DATA_DIR, objectType, prefixedJsonFilename(buildDataFilePrefix(ct), ct)),
110
129
  data: [],
111
130
  };
112
131
  this.collections.set(key, newC);
@@ -123,7 +142,7 @@ export class InMemoryStore {
123
142
  }
124
143
  else {
125
144
  this.collections.set(key, {
126
- path: path.join(DATA_DIR, objectType, `${ct}.json`),
145
+ path: path.join(DATA_DIR, objectType, prefixedJsonFilename(buildDataFilePrefix(ct), ct)),
127
146
  data,
128
147
  [DIRTY]: true,
129
148
  });
@@ -1,5 +1,61 @@
1
1
  # Managing JSON Files
2
2
 
3
+ ## File-backed database routing
4
+
5
+ The static server has no database, so database routing is represented by a
6
+ reserved filename prefix followed by `--`.
7
+
8
+ With:
9
+
10
+ ```bash
11
+ MEMORIX_ORG_ID=neo
12
+ MEMORIX_AGENT_ID=neo-agent
13
+ ```
14
+
15
+ the routing matrix is:
16
+
17
+ | Data kind | Filename prefix | Example |
18
+ | --- | --- | --- |
19
+ | Snapshots, analysis, decisions | `neo-memorix-entities--` | `data/product/neo-memorix-entities--snapshots.json` |
20
+ | Events | `neo-memorix-events--` | `data/product/neo-memorix-events--events.json` |
21
+ | Memory | `neo-agent-memorix-catalox--` | `data/memory/neo-agent-memorix-catalox--memory.json` |
22
+ | Metadata | `neo-agent-memorix-catalox--` | `metadata/lists/neo-agent-memorix-catalox--my-plate.json` |
23
+
24
+ ### Read and write precedence
25
+
26
+ 1. The server first looks for the routed, prefixed file.
27
+ 2. If it does not exist, it reads the matching unprefixed file as seed data.
28
+ 3. Any mutation writes the complete resulting collection to the routed file.
29
+ 4. Unprefixed seed files are never overwritten while routed mode is active.
30
+ 5. Files belonging to a different prefix are ignored by the active process.
31
+
32
+ This makes onboarding migration-safe: existing files such as
33
+ `product/snapshots.json` continue to seed a new tenant, while the first write
34
+ creates `product/neo-memorix-entities--snapshots.json`. Starting with another
35
+ organization ID selects a different file and therefore different state.
36
+
37
+ The delimiter `--` is reserved. Do not use it inside ordinary, unprefixed
38
+ fixture basenames.
39
+
40
+ ### Inspect the active route
41
+
42
+ ```bash
43
+ curl http://localhost:5030/api/explorer/health
44
+ ```
45
+
46
+ The response includes:
47
+
48
+ ```json
49
+ {
50
+ "fileRouting": {
51
+ "entitiesPrefix": "neo-memorix-entities",
52
+ "eventsPrefix": "neo-memorix-events",
53
+ "memoryPrefix": "neo-agent-memorix-catalox",
54
+ "metadataPrefix": "neo-agent-memorix-catalox"
55
+ }
56
+ }
57
+ ```
58
+
3
59
  The mock server stores all state in JSON files under the `mocks/` directory. At
4
60
  startup, every file is loaded into an in-memory store. Mutations happen in
5
61
  memory and are debounced-flushed back to disk automatically.
@@ -121,6 +121,19 @@ correct tenant.
121
121
  The health endpoint returns:
122
122
  - `memorixDb`: `"<orgId>-memorix-entities + <orgId>-memorix-events"`
123
123
  - `cataloxDb`: `"<agentId>-memorix-catalox"`
124
+ - `fileRouting`: the concrete entity, event, memory, and metadata filename
125
+ prefixes selected for this process
126
+
127
+ These are functional routes, not display-only labels. For example:
128
+
129
+ ```bash
130
+ MEMORIX_ORG_ID=neo MEMORIX_AGENT_ID=neo-agent npm start
131
+ ```
132
+
133
+ selects filenames beginning with `neo-memorix-entities--`,
134
+ `neo-memorix-events--`, and `neo-agent-memorix-catalox--`. See
135
+ [managing-json-files.md](./managing-json-files.md#file-backed-database-routing)
136
+ for exact examples and fallback behavior.
124
137
 
125
138
  ### Feature flags
126
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/static-memorix-explorer-api",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Static mock server providing full API parity for the Memorix Explorer API.",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",