@dreamforgestudiollc/emet 0.0.0-stage → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.env.example +189 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CHARTER.md +137 -0
  4. package/LICENSE +22 -0
  5. package/README.md +868 -2
  6. package/docs/AUTHORSHIP.md +26 -0
  7. package/docs/BENCHMARKS.md +261 -0
  8. package/docs/BOOTSTRAP.md +127 -0
  9. package/docs/BOTS.md +83 -0
  10. package/docs/COVERAGE.md +64 -0
  11. package/docs/DEPLOY.md +251 -0
  12. package/docs/DISCIPLINES.md +29 -0
  13. package/docs/GUARDRAILS-AND-MONORAILS.md +108 -0
  14. package/docs/GUIDED-SETUP.md +301 -0
  15. package/docs/PERMISSIONS.md +121 -0
  16. package/docs/ROADMAP.md +62 -0
  17. package/docs/SETUP.md +265 -0
  18. package/docs/SUBKEYS.md +59 -0
  19. package/gems/artifact-index.json +12 -0
  20. package/gems/bot-team-backup/floor.json +15 -0
  21. package/gems/bot-team-backup/gem.json +43 -0
  22. package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
  23. package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
  24. package/gems/fieldnote/floor.json +14 -0
  25. package/gems/fieldnote/gem.json +70 -0
  26. package/gems/fieldnote/runes/fieldnote.js +4 -0
  27. package/gems/lean-six-sigma/CHANGELOG.md +10 -0
  28. package/gems/lean-six-sigma/README.md +92 -0
  29. package/gems/lean-six-sigma/floor.json +31 -0
  30. package/gems/lean-six-sigma/gem.json +1296 -0
  31. package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
  32. package/gems/lean-six-sigma/guides/BELTS.md +52 -0
  33. package/gems/lean-six-sigma/guides/DMADV.md +56 -0
  34. package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
  35. package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
  36. package/gems/lean-six-sigma/guides/LEAN.md +97 -0
  37. package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
  38. package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
  39. package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
  40. package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
  41. package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
  42. package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
  43. package/gems/neurendipity/floor.json +65 -0
  44. package/gems/neurendipity/gem.json +42 -0
  45. package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
  46. package/gems/neurendipity/guides/NEURODIVERGENT_SUPPORT.md +287 -0
  47. package/guides/README.md +3 -0
  48. package/package.json +76 -4
  49. package/server.json +79 -0
  50. package/src/aleph.js +395 -0
  51. package/src/annotations.js +170 -0
  52. package/src/approve-throttle.js +54 -0
  53. package/src/atlas-role.js +58 -0
  54. package/src/attribution.js +555 -0
  55. package/src/conformance.js +688 -0
  56. package/src/content_analyzer.js +450 -0
  57. package/src/database.js +812 -0
  58. package/src/disciplines.js +26 -0
  59. package/src/dispatch.js +382 -0
  60. package/src/env.js +97 -0
  61. package/src/floor.js +228 -0
  62. package/src/gaps.js +509 -0
  63. package/src/gem-check.js +1359 -0
  64. package/src/guides.js +75 -0
  65. package/src/host-reach.js +23 -0
  66. package/src/http-remote.js +313 -0
  67. package/src/index.js +397 -0
  68. package/src/invites.js +167 -0
  69. package/src/layers.js +449 -0
  70. package/src/lessons-to-checks.js +366 -0
  71. package/src/logger.js +300 -0
  72. package/src/metadata.js +61 -0
  73. package/src/module-path.js +32 -0
  74. package/src/mongo-connect.js +91 -0
  75. package/src/named-items.js +208 -0
  76. package/src/oauth-provider.js +838 -0
  77. package/src/owner-approvals.js +281 -0
  78. package/src/persons.js +136 -0
  79. package/src/provenance.js +257 -0
  80. package/src/redact.js +237 -0
  81. package/src/request-log.js +124 -0
  82. package/src/rune-dispatch.js +360 -0
  83. package/src/rune-registry.js +71 -0
  84. package/src/rune-scope.js +42 -0
  85. package/src/scopes.js +100 -0
  86. package/src/security-headers.js +26 -0
  87. package/src/session-graph.js +947 -0
  88. package/src/session.js +1097 -0
  89. package/src/setup.js +1580 -0
  90. package/src/socket.js +545 -0
  91. package/src/startup-pages.js +371 -0
  92. package/src/startup-payload.js +129 -0
  93. package/src/subkey-store.js +357 -0
  94. package/src/subkeys.js +947 -0
  95. package/src/tools.js +4508 -0
  96. package/src/transcript-session.js +106 -0
  97. package/src/validation.js +1187 -0
  98. package/src/version.js +2 -0
  99. package/templates/BOARD.md +72 -0
  100. package/templates/BOTHANDOFF.md +57 -0
  101. package/templates/CHARACTER.md +30 -0
  102. package/templates/HANDOFF.md +63 -0
  103. package/templates/IDENTITY.md +29 -0
  104. package/templates/INDEX.md +82 -0
  105. package/templates/INSTALL.md +29 -0
  106. package/templates/PROCEDURE.md +39 -0
  107. package/templates/ROSTER.md +43 -0
  108. package/templates/SEED.md +65 -0
  109. package/templates/STAGING.md +37 -0
  110. package/templates/STANDARD.md +99 -0
  111. package/templates/THREAD.md +43 -0
  112. package/templates/USER.md +32 -0
  113. package/verification/load-gate/corpus.json +1765 -0
  114. package/verification/load-gate/load-gate.mjs +1246 -0
@@ -0,0 +1,1187 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ *
5
+ * Derived from CASCADE Memory System
6
+ * Copyright (c) 2025-2026 CIPS Corp (C.I.P.S. LLC)
7
+ * Licensed under the MIT License - see LICENSE
8
+ *
9
+ * https://github.com/For-Sunny/cascade-memory-enterprise
10
+ *
11
+ * Validation Module - Comprehensive validation for all MCP inputs
12
+ */
13
+
14
+ import { IMPORTANCE_BUCKETS } from './layers.js';
15
+
16
+ // ============================================================================
17
+ // CONSTANTS - Validation Limits
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Content length limits
22
+ */
23
+ export const CONTENT_LIMITS = {
24
+ MIN_CONTENT_LENGTH: 1,
25
+ MAX_CONTENT_LENGTH: 100000, // 100KB max content
26
+ MAX_CONTEXT_LENGTH: 10000, // 10KB max context
27
+ MAX_QUERY_LENGTH: 1000, // 1KB max query
28
+ MAX_METADATA_SIZE: 50000, // 50KB max metadata JSON
29
+ MAX_STRING_FIELD_LENGTH: 5000, // 5KB for general string fields
30
+ };
31
+
32
+ /**
33
+ * Numeric range limits
34
+ */
35
+ export const NUMERIC_LIMITS = {
36
+ // Importance and emotional intensity are 0-1 scales
37
+ MIN_IMPORTANCE: 0,
38
+ MAX_IMPORTANCE: 1,
39
+ MIN_EMOTIONAL_INTENSITY: 0,
40
+ MAX_EMOTIONAL_INTENSITY: 1,
41
+
42
+ // Query limits
43
+ MIN_LIMIT: 1,
44
+ MAX_LIMIT: 1000,
45
+ DEFAULT_LIMIT: 10,
46
+
47
+ // Timestamp ranges (Unix epoch)
48
+ MIN_TIMESTAMP: 0,
49
+ MAX_TIMESTAMP: 4102444800, // Year 2100
50
+
51
+ // ID ranges
52
+ MIN_ID: 1,
53
+ MAX_ID: Number.MAX_SAFE_INTEGER,
54
+ };
55
+
56
+ /**
57
+ * Valid memory layers - strict whitelist
58
+ */
59
+ export const VALID_LAYERS = Object.freeze([
60
+ 'episodic',
61
+ 'semantic',
62
+ 'procedural',
63
+ 'meta',
64
+ 'identity',
65
+ 'working'
66
+ ]);
67
+
68
+ /**
69
+ * Valid metadata field names - whitelist for safety
70
+ */
71
+ export const VALID_METADATA_FIELDS = Object.freeze([
72
+ 'importance',
73
+ 'emotional_intensity',
74
+ 'context',
75
+ 'layer',
76
+ 'tags',
77
+ 'source',
78
+ 'person_id',
79
+ 'session_id',
80
+ 'timestamp',
81
+ 'related_ids',
82
+ 'category',
83
+ 'priority',
84
+ 'expires_at',
85
+ // EMET's own vocabulary (2026-09-07). The whitelist above is inherited from
86
+ // upstream and did not know these, so reviseMemory's `supersedes` and every
87
+ // `valid_until` were silently relocated under `custom` - the whole origin of
88
+ // the "placement convention" and of the dual-path readers in tools.js.
89
+ // Nobody chose that; the whitelist did. These are first-class now.
90
+ 'supersedes', // positive integer: id of the entry this one replaces
91
+ 'corrects', // positive integer: id of the entry (same layer) this one partly corrects; the parent stays live
92
+ 'superseded_by', // positive integer or null: id of the entry that replaced this one
93
+ 'superseded_at', // Unix seconds
94
+ 'valid_until', // Unix seconds: an end date. The entry stays readable, marked ended.
95
+ 'valid_from', // Unix seconds: must not exceed valid_until when both present
96
+ 'routing', // plain object: the routing decision that placed the entry
97
+ 'source_defaulted', // boolean: the source tag came from the connector default
98
+ 'assertion_origin', // 'user' | 'assistant' | 'consolidated' | 'imported' | null
99
+ 'content_origin', // 'self' | 'assistant' | 'external' — who the TEXT came from
100
+ // Accountability markers (2026-09-07, receipt-as-fixture). Every field that
101
+ // is MISSING is written as a visible marker, never left absent: absence
102
+ // cannot be queried, "missing" can. Set by saveMemory, never by the caller.
103
+ 'source_missing', // boolean: no source given and no default configured
104
+ 'source_unregistered', // boolean: source is not in EMET_SOURCE_TAGS
105
+ 'importance_defaulted', // boolean: importance came from the layer's charter midpoint
106
+ 'importance_bucket', // constitutive|durable|binding|event|scratch
107
+ 'importance_degree', // low|mid|high — only when degree inside the bucket matters
108
+ 'provenance_missing', // boolean: no derived_from on a layer whose charter wants one
109
+ 'assertion_origin_missing', // boolean: assertion_origin was not supplied at all
110
+ // Kept RUNE results (GEP-0003). The server sets these on a real seal.
111
+ // `version` alone is an ordinary key. `rune_attested` marks a server seal.
112
+ 'gem',
113
+ 'version',
114
+ 'rune',
115
+ 'served_sha256',
116
+ 'client_sha256',
117
+ 'input_sha256',
118
+ 'input_ref',
119
+ 'principal',
120
+ 'rune_attested',
121
+ 'custom' // Allow a 'custom' object for extensibility
122
+ ]);
123
+
124
+ /**
125
+ * The boolean accountability markers - validated identically.
126
+ */
127
+ export const MARKER_FIELDS = Object.freeze([
128
+ 'corrected_by', // option A (2026-10-02): written on the PARENT by markCorrectedBy, never by a caller
129
+ 'redacted_spans', // E4: written by the door from the scrubber's count, never by a caller
130
+ 'source_defaulted',
131
+ 'source_missing',
132
+ 'source_unregistered',
133
+ 'importance_defaulted',
134
+ 'provenance_missing',
135
+ 'assertion_origin_missing'
136
+ ]);
137
+
138
+ /**
139
+ * Valid values for metadata.assertion_origin (null is also accepted).
140
+ */
141
+ export const ASSERTION_ORIGINS = Object.freeze(['user', 'assistant', 'consolidated', 'imported']);
142
+ export const CONTENT_ORIGINS = Object.freeze(['self', 'assistant', 'external']);
143
+
144
+ // ============================================================================
145
+ // VALIDATION ERROR CLASS
146
+ // ============================================================================
147
+
148
+ /**
149
+ * Custom validation error with field information
150
+ */
151
+ export class ValidationError extends Error {
152
+ constructor(field, message, value = undefined) {
153
+ // One-argument calls pass the whole message as `field` (the reviewer's fix (b), 2026-10-02): that used to
154
+ // render "Validation failed for '<message>': undefined".
155
+ if (message === undefined) { message = field; field = null; }
156
+ super(field === null ? message : `Validation failed for '${field}': ${message}`);
157
+ this.name = 'ValidationError';
158
+ this.field = field;
159
+ this.validationMessage = message;
160
+ // Don't expose the actual value in production for security
161
+ this.invalidValue = process.env.DEBUG === 'true' ? value : '[hidden]';
162
+ }
163
+
164
+ toJSON() {
165
+ return {
166
+ error: 'ValidationError',
167
+ field: this.field,
168
+ message: this.validationMessage
169
+ };
170
+ }
171
+ }
172
+
173
+ // ============================================================================
174
+ // TYPE VALIDATORS
175
+ // ============================================================================
176
+
177
+ /**
178
+ * Check if value is a string
179
+ */
180
+ export function isString(value) {
181
+ return typeof value === 'string';
182
+ }
183
+
184
+ /**
185
+ * Check if value is a number (finite, not NaN)
186
+ */
187
+ export function isNumber(value) {
188
+ return typeof value === 'number' && Number.isFinite(value);
189
+ }
190
+
191
+ /**
192
+ * Check if value is an integer
193
+ */
194
+ export function isInteger(value) {
195
+ return Number.isInteger(value);
196
+ }
197
+
198
+ /**
199
+ * Check if value is a plain object
200
+ */
201
+ export function isPlainObject(value) {
202
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
203
+ }
204
+
205
+ /**
206
+ * Check if value is an array
207
+ */
208
+ export function isArray(value) {
209
+ return Array.isArray(value);
210
+ }
211
+
212
+ // ============================================================================
213
+ // CONTENT VALIDATORS
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Validate content string
218
+ * @param {*} content - Content to validate
219
+ * @param {string} fieldName - Field name for error messages
220
+ * @returns {string} - Validated and trimmed content
221
+ * @throws {ValidationError}
222
+ */
223
+ export function validateContent(content, fieldName = 'content') {
224
+ if (content === null || content === undefined) {
225
+ throw new ValidationError(fieldName, 'Content is required');
226
+ }
227
+
228
+ if (!isString(content)) {
229
+ throw new ValidationError(fieldName, 'Content must be a string', typeof content);
230
+ }
231
+
232
+ const trimmed = content.trim();
233
+
234
+ if (trimmed.length < CONTENT_LIMITS.MIN_CONTENT_LENGTH) {
235
+ throw new ValidationError(fieldName, `Content must be at least ${CONTENT_LIMITS.MIN_CONTENT_LENGTH} character(s)`);
236
+ }
237
+
238
+ if (trimmed.length > CONTENT_LIMITS.MAX_CONTENT_LENGTH) {
239
+ throw new ValidationError(
240
+ fieldName,
241
+ `Content exceeds maximum length of ${CONTENT_LIMITS.MAX_CONTENT_LENGTH} characters (got ${trimmed.length})`
242
+ );
243
+ }
244
+
245
+ return trimmed;
246
+ }
247
+
248
+ /**
249
+ * Validate query string
250
+ * @param {*} query - Query to validate
251
+ * @returns {string} - Validated and trimmed query
252
+ * @throws {ValidationError}
253
+ */
254
+ export function validateQuery(query) {
255
+ if (query === null || query === undefined) {
256
+ throw new ValidationError('query', 'Query is required');
257
+ }
258
+
259
+ if (!isString(query)) {
260
+ throw new ValidationError('query', 'Query must be a string', typeof query);
261
+ }
262
+
263
+ const trimmed = query.trim();
264
+
265
+ if (trimmed.length < 1) {
266
+ throw new ValidationError('query', 'Query cannot be empty');
267
+ }
268
+
269
+ if (trimmed.length > CONTENT_LIMITS.MAX_QUERY_LENGTH) {
270
+ throw new ValidationError(
271
+ 'query',
272
+ `Query exceeds maximum length of ${CONTENT_LIMITS.MAX_QUERY_LENGTH} characters (got ${trimmed.length})`
273
+ );
274
+ }
275
+
276
+ return trimmed;
277
+ }
278
+
279
+ /**
280
+ * Validate context string (optional field)
281
+ * @param {*} context - Context to validate
282
+ * @returns {string} - Validated context or empty string
283
+ * @throws {ValidationError}
284
+ */
285
+ export function validateContext(context) {
286
+ if (context === null || context === undefined || context === '') {
287
+ return '';
288
+ }
289
+
290
+ if (!isString(context)) {
291
+ throw new ValidationError('context', 'Context must be a string', typeof context);
292
+ }
293
+
294
+ if (context.length > CONTENT_LIMITS.MAX_CONTEXT_LENGTH) {
295
+ throw new ValidationError(
296
+ 'context',
297
+ `Context exceeds maximum length of ${CONTENT_LIMITS.MAX_CONTEXT_LENGTH} characters (got ${context.length})`
298
+ );
299
+ }
300
+
301
+ return context;
302
+ }
303
+
304
+ // ============================================================================
305
+ // LAYER VALIDATORS
306
+ // ============================================================================
307
+
308
+ /**
309
+ * Validate memory layer name (strict whitelist)
310
+ * @param {*} layer - Layer name to validate
311
+ * @param {boolean} required - Whether layer is required
312
+ * @returns {string|null} - Validated layer or null if not required and not provided
313
+ * @throws {ValidationError}
314
+ */
315
+ export function validateLayer(layer, required = false) {
316
+ if (layer === null || layer === undefined || layer === '') {
317
+ if (required) {
318
+ throw new ValidationError('layer', 'Layer is required');
319
+ }
320
+ return null;
321
+ }
322
+
323
+ if (!isString(layer)) {
324
+ throw new ValidationError('layer', 'Layer must be a string', typeof layer);
325
+ }
326
+
327
+ const normalizedLayer = layer.toLowerCase().trim();
328
+
329
+ if (!VALID_LAYERS.includes(normalizedLayer)) {
330
+ throw new ValidationError(
331
+ 'layer',
332
+ `Invalid layer '${normalizedLayer}'. Must be one of: ${VALID_LAYERS.join(', ')}`
333
+ );
334
+ }
335
+
336
+ return normalizedLayer;
337
+ }
338
+
339
+ // ============================================================================
340
+ // NUMERIC VALIDATORS
341
+ // ============================================================================
342
+
343
+ /**
344
+ * Validate a number is within a range
345
+ * @param {*} value - Value to validate
346
+ * @param {string} fieldName - Field name for error messages
347
+ * @param {number} min - Minimum value (inclusive)
348
+ * @param {number} max - Maximum value (inclusive)
349
+ * @param {number|null} defaultValue - Default if not provided
350
+ * @returns {number} - Validated number
351
+ * @throws {ValidationError}
352
+ */
353
+ export function validateNumericRange(value, fieldName, min, max, defaultValue = null) {
354
+ if (value === null || value === undefined) {
355
+ if (defaultValue !== null) {
356
+ return defaultValue;
357
+ }
358
+ throw new ValidationError(fieldName, 'Value is required');
359
+ }
360
+
361
+ // Handle string numbers (e.g., from JSON parsing edge cases)
362
+ let numValue = value;
363
+ if (isString(value)) {
364
+ numValue = parseFloat(value);
365
+ }
366
+
367
+ if (!isNumber(numValue)) {
368
+ throw new ValidationError(fieldName, 'Value must be a number', value);
369
+ }
370
+
371
+ if (numValue < min || numValue > max) {
372
+ throw new ValidationError(
373
+ fieldName,
374
+ `Value must be between ${min} and ${max} (got ${numValue})`
375
+ );
376
+ }
377
+
378
+ return numValue;
379
+ }
380
+
381
+ /**
382
+ * Validate importance field (0-1 scale)
383
+ */
384
+ export function validateImportance(value) {
385
+ return validateNumericRange(
386
+ value,
387
+ 'importance',
388
+ NUMERIC_LIMITS.MIN_IMPORTANCE,
389
+ NUMERIC_LIMITS.MAX_IMPORTANCE,
390
+ 0.7 // Default importance
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Validate emotional intensity field (0-1 scale)
396
+ */
397
+ export function validateEmotionalIntensity(value) {
398
+ return validateNumericRange(
399
+ value,
400
+ 'emotional_intensity',
401
+ NUMERIC_LIMITS.MIN_EMOTIONAL_INTENSITY,
402
+ NUMERIC_LIMITS.MAX_EMOTIONAL_INTENSITY,
403
+ 0.5 // Default emotional intensity
404
+ );
405
+ }
406
+
407
+ /**
408
+ * Validate limit field (query results limit)
409
+ */
410
+ export function validateLimit(value) {
411
+ const limit = validateNumericRange(
412
+ value,
413
+ 'limit',
414
+ NUMERIC_LIMITS.MIN_LIMIT,
415
+ NUMERIC_LIMITS.MAX_LIMIT,
416
+ NUMERIC_LIMITS.DEFAULT_LIMIT
417
+ );
418
+ return Math.floor(limit); // Ensure integer
419
+ }
420
+
421
+ /**
422
+ * Validate timestamp field
423
+ */
424
+ export function validateTimestamp(value, fieldName = 'timestamp') {
425
+ if (value === null || value === undefined) {
426
+ return null; // Timestamps are optional in filters
427
+ }
428
+
429
+ return validateNumericRange(
430
+ value,
431
+ fieldName,
432
+ NUMERIC_LIMITS.MIN_TIMESTAMP,
433
+ NUMERIC_LIMITS.MAX_TIMESTAMP,
434
+ null
435
+ );
436
+ }
437
+
438
+ /**
439
+ * Validate ID field
440
+ */
441
+ export function validateId(value) {
442
+ if (value === null || value === undefined) {
443
+ return null; // IDs are optional in filters
444
+ }
445
+
446
+ const id = validateNumericRange(
447
+ value,
448
+ 'id',
449
+ NUMERIC_LIMITS.MIN_ID,
450
+ NUMERIC_LIMITS.MAX_ID,
451
+ null
452
+ );
453
+ return Math.floor(id); // Ensure integer
454
+ }
455
+
456
+ // ============================================================================
457
+ // METADATA VALIDATORS
458
+ // ============================================================================
459
+
460
+ /**
461
+ * Validate metadata object
462
+ * @param {*} metadata - Metadata to validate
463
+ * @returns {object} - Validated and sanitized metadata
464
+ * @throws {ValidationError}
465
+ */
466
+ export function validateMetadata(metadata) {
467
+ if (metadata === null || metadata === undefined) {
468
+ return {};
469
+ }
470
+
471
+ if (!isPlainObject(metadata)) {
472
+ throw new ValidationError('metadata', 'Metadata must be an object', typeof metadata);
473
+ }
474
+
475
+ // Check total size of metadata JSON
476
+ const metadataJson = JSON.stringify(metadata);
477
+ if (metadataJson.length > CONTENT_LIMITS.MAX_METADATA_SIZE) {
478
+ throw new ValidationError(
479
+ 'metadata',
480
+ `Metadata exceeds maximum size of ${CONTENT_LIMITS.MAX_METADATA_SIZE} bytes (got ${metadataJson.length})`
481
+ );
482
+ }
483
+
484
+ const sanitized = {};
485
+ const relocated = [];
486
+
487
+ for (const [key, value] of Object.entries(metadata)) {
488
+ // Validate field name against whitelist (or allow if it's under 'custom')
489
+ if (!VALID_METADATA_FIELDS.includes(key) && key !== 'custom') {
490
+ // Log warning but don't reject - place unknown fields under 'custom'.
491
+ // The name is recorded so the write receipt can say what moved (A6).
492
+ if (!sanitized.custom) {
493
+ sanitized.custom = {};
494
+ }
495
+ sanitized.custom[key] = value;
496
+ relocated.push(key);
497
+ continue;
498
+ }
499
+
500
+ // Validate specific known fields
501
+ switch (key) {
502
+ case 'importance':
503
+ sanitized.importance = validateImportance(value);
504
+ break;
505
+
506
+ case 'emotional_intensity':
507
+ sanitized.emotional_intensity = validateEmotionalIntensity(value);
508
+ break;
509
+
510
+ case 'context':
511
+ sanitized.context = validateContext(value);
512
+ break;
513
+
514
+ case 'layer':
515
+ sanitized.layer = validateLayer(value, false);
516
+ break;
517
+
518
+ case 'timestamp':
519
+ sanitized.timestamp = validateTimestamp(value);
520
+ break;
521
+
522
+ case 'tags':
523
+ sanitized.tags = validateTags(value);
524
+ break;
525
+
526
+ case 'source':
527
+ sanitized.source = validateStringField(value, 'source');
528
+ break;
529
+
530
+ case 'session_id':
531
+ // A2 (2026-09-07): a bare session_id is not provenance - it names a
532
+ // session without a span, which is the nearest-timestamp guess the
533
+ // provenance rule forbids. Rejected, with the door pointing at the field
534
+ // that carries a checkable pointer.
535
+ throw new ValidationError(
536
+ 'session_id',
537
+ 'metadata.session_id is not accepted: supply derived_from {session_id, exchange_start, exchange_end} ' +
538
+ '(a transcript span) or derived_from {entries: [...]} (a consolidation) instead',
539
+ typeof value
540
+ );
541
+
542
+ case 'category':
543
+ sanitized.category = validateStringField(value, 'category');
544
+ break;
545
+
546
+ case 'priority':
547
+ sanitized.priority = validateNumericRange(value, 'priority', 0, 100, 50);
548
+ break;
549
+
550
+ case 'expires_at': {
551
+ // A2 (2026-09-07): `valid_until` is canonical; `expires_at` is the
552
+ // upstream (CASCADE) spelling and is accepted as an alias of it. Two
553
+ // spellings with two values is a contradiction, not a choice.
554
+ const aliased = validateTimestamp(value, 'expires_at');
555
+ if (sanitized.valid_until !== undefined && sanitized.valid_until !== null &&
556
+ aliased !== null && sanitized.valid_until !== aliased) {
557
+ throw new ValidationError('expires_at', `expires_at (${aliased}) disagrees with valid_until (${sanitized.valid_until}); supply one`, typeof value);
558
+ }
559
+ if (sanitized.valid_until === undefined || sanitized.valid_until === null) sanitized.valid_until = aliased;
560
+ break;
561
+ }
562
+
563
+ case 'related_ids':
564
+ sanitized.related_ids = validateIdArray(value);
565
+ break;
566
+
567
+ // ---- EMET vocabulary (first-class, never relocated) ----
568
+ case 'supersedes':
569
+ sanitized.supersedes = validatePositiveInteger(value, 'supersedes');
570
+ break;
571
+
572
+ case 'corrects':
573
+ sanitized.corrects = validatePositiveInteger(value, 'corrects');
574
+ break;
575
+
576
+ case 'superseded_by':
577
+ sanitized.superseded_by = (value === null || value === undefined)
578
+ ? null
579
+ : validatePositiveInteger(value, 'superseded_by');
580
+ break;
581
+
582
+ case 'superseded_at':
583
+ sanitized.superseded_at = validateTimestamp(value, 'superseded_at');
584
+ break;
585
+
586
+ case 'valid_until': {
587
+ const canonical = validateTimestamp(value, 'valid_until');
588
+ // If expires_at was seen first, valid_until is already set from the alias.
589
+ if (sanitized.valid_until !== undefined && sanitized.valid_until !== null &&
590
+ canonical !== null && sanitized.valid_until !== canonical) {
591
+ throw new ValidationError('valid_until', `valid_until (${canonical}) disagrees with expires_at (${sanitized.valid_until}); supply one`, typeof value);
592
+ }
593
+ sanitized.valid_until = canonical;
594
+ break;
595
+ }
596
+
597
+ case 'valid_from':
598
+ sanitized.valid_from = validateTimestamp(value, 'valid_from');
599
+ break;
600
+
601
+ case 'routing':
602
+ if (value === null || value === undefined) {
603
+ sanitized.routing = null;
604
+ } else if (!isPlainObject(value)) {
605
+ throw new ValidationError('routing', 'Routing must be a plain object', typeof value);
606
+ } else {
607
+ sanitized.routing = value;
608
+ }
609
+ break;
610
+
611
+ case 'source_defaulted':
612
+ case 'source_missing':
613
+ case 'source_unregistered':
614
+ case 'importance_defaulted':
615
+ case 'provenance_missing':
616
+ case 'assertion_origin_missing':
617
+ case 'rune_attested':
618
+ if (typeof value !== 'boolean') {
619
+ throw new ValidationError(key, 'Value must be a boolean', typeof value);
620
+ }
621
+ sanitized[key] = value;
622
+ break;
623
+
624
+ case 'assertion_origin':
625
+ if (value === null || value === undefined) {
626
+ sanitized.assertion_origin = null;
627
+ } else if (!isString(value) || !ASSERTION_ORIGINS.includes(value)) {
628
+ throw new ValidationError(
629
+ 'assertion_origin',
630
+ `Invalid assertion_origin '${value}'. Must be one of: ${ASSERTION_ORIGINS.join(', ')} (or null)`
631
+ );
632
+ } else {
633
+ sanitized.assertion_origin = value;
634
+ }
635
+ break;
636
+
637
+ case 'gem':
638
+ case 'version':
639
+ case 'rune':
640
+ case 'served_sha256':
641
+ case 'client_sha256':
642
+ case 'input_sha256':
643
+ case 'input_ref':
644
+ case 'principal':
645
+ if (typeof value !== 'string') throw new ValidationError(key, 'Value must be a string', typeof value);
646
+ sanitized[key] = value;
647
+ break;
648
+
649
+ case 'custom':
650
+ // Allow custom object but validate its size
651
+ if (isPlainObject(value)) {
652
+ const customJson = JSON.stringify(value);
653
+ if (customJson.length <= CONTENT_LIMITS.MAX_METADATA_SIZE / 2) {
654
+ sanitized.custom = { ...sanitized.custom, ...value };
655
+ } else {
656
+ throw new ValidationError('custom', 'Custom metadata object is too large');
657
+ }
658
+ }
659
+ break;
660
+
661
+ default:
662
+ // Should not reach here due to whitelist check above
663
+ sanitized[key] = value;
664
+ }
665
+ }
666
+
667
+ // Cross-validation: a validity window cannot close before it opens
668
+ if (sanitized.valid_from !== undefined && sanitized.valid_from !== null &&
669
+ sanitized.valid_until !== undefined && sanitized.valid_until !== null) {
670
+ if (sanitized.valid_from > sanitized.valid_until) {
671
+ throw new ValidationError(
672
+ 'valid_from',
673
+ `valid_from (${sanitized.valid_from}) cannot be greater than valid_until (${sanitized.valid_until})`
674
+ );
675
+ }
676
+ }
677
+
678
+ if (relocated.length > 0) {
679
+ RELOCATED_FIELDS.set(sanitized, relocated);
680
+ }
681
+
682
+ return sanitized;
683
+ }
684
+
685
+ /**
686
+ * Names of the fields validateMetadata moved under `custom`, keyed by the
687
+ * object it returned. A WeakMap rather than a property on the result so the
688
+ * return shape is unchanged for every existing caller and nothing extra is
689
+ * ever serialized into the store.
690
+ */
691
+ const RELOCATED_FIELDS = new WeakMap();
692
+
693
+ /**
694
+ * Which top-level metadata fields validateMetadata relocated under `custom`.
695
+ * @param {object} validated - the object validateMetadata returned
696
+ * @returns {string[]} - field names, in input order; empty when nothing moved
697
+ */
698
+ export function relocatedFields(validated) {
699
+ const names = RELOCATED_FIELDS.get(validated);
700
+ return names ? [...names] : [];
701
+ }
702
+
703
+ /**
704
+ * Validate a positive integer id-like field (supersedes, superseded_by)
705
+ * @param {*} value - Value to validate
706
+ * @param {string} fieldName - Field name for error messages
707
+ * @returns {number} - Validated positive integer
708
+ * @throws {ValidationError}
709
+ */
710
+ export function validatePositiveInteger(value, fieldName) {
711
+ if (value === null || value === undefined) {
712
+ throw new ValidationError(fieldName, 'Value is required');
713
+ }
714
+
715
+ let numValue = value;
716
+ if (isString(value) && value.trim() !== '') {
717
+ numValue = Number(value);
718
+ }
719
+
720
+ if (!isNumber(numValue) || !isInteger(numValue) || numValue < NUMERIC_LIMITS.MIN_ID) {
721
+ throw new ValidationError(fieldName, 'Value must be a positive integer', value);
722
+ }
723
+
724
+ return numValue;
725
+ }
726
+
727
+ /**
728
+ * Validate tags array
729
+ * @param {*} tags - Tags to validate
730
+ * @returns {string[]} - Validated tags array
731
+ * @throws {ValidationError}
732
+ */
733
+ export function validateTags(tags) {
734
+ if (tags === null || tags === undefined) {
735
+ return [];
736
+ }
737
+
738
+ if (!isArray(tags)) {
739
+ throw new ValidationError('tags', 'Tags must be an array', typeof tags);
740
+ }
741
+
742
+ if (tags.length > 50) {
743
+ throw new ValidationError('tags', 'Too many tags (max 50)');
744
+ }
745
+
746
+ const validatedTags = [];
747
+ for (let i = 0; i < tags.length; i++) {
748
+ const tag = tags[i];
749
+ if (!isString(tag)) {
750
+ throw new ValidationError(`tags[${i}]`, 'Each tag must be a string', typeof tag);
751
+ }
752
+ const trimmed = tag.trim();
753
+ if (trimmed.length > 100) {
754
+ throw new ValidationError(`tags[${i}]`, 'Tag exceeds maximum length of 100 characters');
755
+ }
756
+ if (trimmed.length > 0) {
757
+ validatedTags.push(trimmed);
758
+ }
759
+ }
760
+
761
+ return validatedTags;
762
+ }
763
+
764
+ /**
765
+ * Validate ID array
766
+ * @param {*} ids - IDs to validate
767
+ * @returns {number[]} - Validated IDs array
768
+ * @throws {ValidationError}
769
+ */
770
+ export function validateIdArray(ids) {
771
+ if (ids === null || ids === undefined) {
772
+ return [];
773
+ }
774
+
775
+ if (!isArray(ids)) {
776
+ throw new ValidationError('related_ids', 'Related IDs must be an array', typeof ids);
777
+ }
778
+
779
+ if (ids.length > 100) {
780
+ throw new ValidationError('related_ids', 'Too many related IDs (max 100)');
781
+ }
782
+
783
+ const validatedIds = [];
784
+ for (let i = 0; i < ids.length; i++) {
785
+ const id = validateNumericRange(
786
+ ids[i],
787
+ `related_ids[${i}]`,
788
+ NUMERIC_LIMITS.MIN_ID,
789
+ NUMERIC_LIMITS.MAX_ID,
790
+ null
791
+ );
792
+ if (id !== null) {
793
+ validatedIds.push(Math.floor(id));
794
+ }
795
+ }
796
+
797
+ return validatedIds;
798
+ }
799
+
800
+ /**
801
+ * Validate generic string field
802
+ * @param {*} value - Value to validate
803
+ * @param {string} fieldName - Field name for error messages
804
+ * @returns {string} - Validated string
805
+ * @throws {ValidationError}
806
+ */
807
+ export function validateStringField(value, fieldName) {
808
+ if (value === null || value === undefined) {
809
+ return '';
810
+ }
811
+
812
+ if (!isString(value)) {
813
+ throw new ValidationError(fieldName, 'Value must be a string', typeof value);
814
+ }
815
+
816
+ if (value.length > CONTENT_LIMITS.MAX_STRING_FIELD_LENGTH) {
817
+ throw new ValidationError(
818
+ fieldName,
819
+ `Value exceeds maximum length of ${CONTENT_LIMITS.MAX_STRING_FIELD_LENGTH} characters`
820
+ );
821
+ }
822
+
823
+ return value;
824
+ }
825
+
826
+ // ============================================================================
827
+ // FILTER VALIDATORS
828
+ // ============================================================================
829
+
830
+ /**
831
+ * The filter keys validateFilters recognises. Everything else in a filters
832
+ * object is discarded silently by design - the validator is a whitelist - so a
833
+ * caller who passes an unrecognised key gets a full UNFILTERED page back and no
834
+ * error, which is indistinguishable from "the filter did not work". That
835
+ * symptom cost a bug hunt on 2026-09-08 (semantic ref 259). The list is exported
836
+ * so callers can NAME the dropped keys instead of swallowing them.
837
+ */
838
+ export const RECOGNISED_FILTER_KEYS = Object.freeze([
839
+ 'importance_min',
840
+ 'importance_max',
841
+ 'importance_bucket',
842
+ 'emotional_intensity_min',
843
+ 'emotional_intensity_max',
844
+ 'timestamp_after',
845
+ 'timestamp_before',
846
+ 'content_contains',
847
+ 'context_contains',
848
+ 'id'
849
+ ]);
850
+
851
+ /**
852
+ * Return the keys of a filters object that validateFilters will discard.
853
+ * Non-objects yield an empty list; shape errors are validateFilters's to throw.
854
+ * @param {*} filters
855
+ * @returns {string[]}
856
+ */
857
+ export function unrecognisedFilterKeys(filters) {
858
+ if (!isPlainObject(filters)) {
859
+ return [];
860
+ }
861
+ return Object.keys(filters).filter((key) => !RECOGNISED_FILTER_KEYS.includes(key));
862
+ }
863
+
864
+ /**
865
+ * Validate query filters object
866
+ * @param {*} filters - Filters to validate
867
+ * @returns {object} - Validated filters
868
+ * @throws {ValidationError}
869
+ */
870
+ export function validateFilters(filters) {
871
+ if (filters === null || filters === undefined) {
872
+ return {};
873
+ }
874
+
875
+ if (!isPlainObject(filters)) {
876
+ throw new ValidationError('filters', 'Filters must be an object', typeof filters);
877
+ }
878
+
879
+ const validated = {};
880
+
881
+ // Importance filters
882
+ if ('importance_min' in filters) {
883
+ validated.importance_min = validateNumericRange(
884
+ filters.importance_min,
885
+ 'filters.importance_min',
886
+ NUMERIC_LIMITS.MIN_IMPORTANCE,
887
+ NUMERIC_LIMITS.MAX_IMPORTANCE,
888
+ null
889
+ );
890
+ }
891
+
892
+ if ('importance_max' in filters) {
893
+ validated.importance_max = validateNumericRange(
894
+ filters.importance_max,
895
+ 'filters.importance_max',
896
+ NUMERIC_LIMITS.MIN_IMPORTANCE,
897
+ NUMERIC_LIMITS.MAX_IMPORTANCE,
898
+ null
899
+ );
900
+ }
901
+
902
+ if ('importance_bucket' in filters) {
903
+ const name = String(filters.importance_bucket).trim().toLowerCase();
904
+ const spec = IMPORTANCE_BUCKETS[name];
905
+ if (!spec) {
906
+ throw new ValidationError(
907
+ 'filters.importance_bucket',
908
+ `Unknown bucket '${filters.importance_bucket}'. Use constitutive, durable, binding, event, or scratch.`
909
+ );
910
+ }
911
+ validated.importance_bucket = name;
912
+ if (validated.importance_min === undefined) validated.importance_min = spec.range[0];
913
+ if (validated.importance_max === undefined) validated.importance_max = spec.range[1];
914
+ }
915
+
916
+ // Emotional intensity filters
917
+ if ('emotional_intensity_min' in filters) {
918
+ validated.emotional_intensity_min = validateNumericRange(
919
+ filters.emotional_intensity_min,
920
+ 'filters.emotional_intensity_min',
921
+ NUMERIC_LIMITS.MIN_EMOTIONAL_INTENSITY,
922
+ NUMERIC_LIMITS.MAX_EMOTIONAL_INTENSITY,
923
+ null
924
+ );
925
+ }
926
+
927
+ if ('emotional_intensity_max' in filters) {
928
+ validated.emotional_intensity_max = validateNumericRange(
929
+ filters.emotional_intensity_max,
930
+ 'filters.emotional_intensity_max',
931
+ NUMERIC_LIMITS.MIN_EMOTIONAL_INTENSITY,
932
+ NUMERIC_LIMITS.MAX_EMOTIONAL_INTENSITY,
933
+ null
934
+ );
935
+ }
936
+
937
+ // Timestamp filters
938
+ if ('timestamp_after' in filters) {
939
+ validated.timestamp_after = validateTimestamp(filters.timestamp_after, 'filters.timestamp_after');
940
+ }
941
+
942
+ if ('timestamp_before' in filters) {
943
+ validated.timestamp_before = validateTimestamp(filters.timestamp_before, 'filters.timestamp_before');
944
+ }
945
+
946
+ // Content search filters
947
+ if ('content_contains' in filters) {
948
+ validated.content_contains = validateStringField(filters.content_contains, 'filters.content_contains');
949
+ if (validated.content_contains.length > CONTENT_LIMITS.MAX_QUERY_LENGTH) {
950
+ throw new ValidationError(
951
+ 'filters.content_contains',
952
+ `Search term exceeds maximum length of ${CONTENT_LIMITS.MAX_QUERY_LENGTH}`
953
+ );
954
+ }
955
+ }
956
+
957
+ if ('context_contains' in filters) {
958
+ validated.context_contains = validateStringField(filters.context_contains, 'filters.context_contains');
959
+ if (validated.context_contains.length > CONTENT_LIMITS.MAX_QUERY_LENGTH) {
960
+ throw new ValidationError(
961
+ 'filters.context_contains',
962
+ `Search term exceeds maximum length of ${CONTENT_LIMITS.MAX_QUERY_LENGTH}`
963
+ );
964
+ }
965
+ }
966
+
967
+ // ID filter
968
+ if ('id' in filters) {
969
+ validated.id = validateId(filters.id);
970
+ }
971
+
972
+ // Cross-validation: min should not exceed max
973
+ if (validated.importance_min !== undefined && validated.importance_max !== undefined) {
974
+ if (validated.importance_min > validated.importance_max) {
975
+ throw new ValidationError(
976
+ 'filters',
977
+ 'importance_min cannot be greater than importance_max'
978
+ );
979
+ }
980
+ }
981
+
982
+ if (validated.emotional_intensity_min !== undefined && validated.emotional_intensity_max !== undefined) {
983
+ if (validated.emotional_intensity_min > validated.emotional_intensity_max) {
984
+ throw new ValidationError(
985
+ 'filters',
986
+ 'emotional_intensity_min cannot be greater than emotional_intensity_max'
987
+ );
988
+ }
989
+ }
990
+
991
+ if (validated.timestamp_after !== undefined && validated.timestamp_before !== undefined) {
992
+ if (validated.timestamp_after > validated.timestamp_before) {
993
+ throw new ValidationError(
994
+ 'filters',
995
+ 'timestamp_after cannot be greater than timestamp_before'
996
+ );
997
+ }
998
+ }
999
+
1000
+ return validated;
1001
+ }
1002
+
1003
+ // ============================================================================
1004
+ // OPTIONS VALIDATORS
1005
+ // ============================================================================
1006
+
1007
+ /**
1008
+ * Validate query_layer options
1009
+ * @param {*} options - Options to validate
1010
+ * @returns {object} - Validated options
1011
+ * @throws {ValidationError}
1012
+ */
1013
+ export function validateQueryOptions(options) {
1014
+ if (options === null || options === undefined) {
1015
+ return { limit: NUMERIC_LIMITS.DEFAULT_LIMIT };
1016
+ }
1017
+
1018
+ if (!isPlainObject(options)) {
1019
+ throw new ValidationError('options', 'Options must be an object', typeof options);
1020
+ }
1021
+
1022
+ const validated = {};
1023
+
1024
+ // Validate limit
1025
+ if ('limit' in options) {
1026
+ validated.limit = validateLimit(options.limit);
1027
+ } else {
1028
+ validated.limit = NUMERIC_LIMITS.DEFAULT_LIMIT;
1029
+ }
1030
+
1031
+ // Validate order_by (already sanitized in main code, but validate format here)
1032
+ if ('order_by' in options) {
1033
+ if (!isString(options.order_by)) {
1034
+ throw new ValidationError('options.order_by', 'order_by must be a string');
1035
+ }
1036
+ validated.order_by = options.order_by.trim();
1037
+ }
1038
+
1039
+ // Validate filters
1040
+ if ('filters' in options) {
1041
+ validated.filters = validateFilters(options.filters);
1042
+ // validateFilters is a whitelist: unrecognised keys vanish without error and
1043
+ // the query then runs unfiltered. Carry the dropped names up so the caller
1044
+ // can report them, mirroring the deprecated-'where' flag below.
1045
+ const droppedFilterKeys = unrecognisedFilterKeys(options.filters);
1046
+ if (droppedFilterKeys.length > 0) {
1047
+ validated._unknown_filter_keys = droppedFilterKeys;
1048
+ }
1049
+ }
1050
+
1051
+ // Legacy params support (deprecated but validated)
1052
+ if ('params' in options) {
1053
+ if (isArray(options.params)) {
1054
+ validated.params = options.params.slice(0, 10); // Limit params count
1055
+ }
1056
+ }
1057
+
1058
+ // Ignore deprecated 'where' field - log warning handled in main code
1059
+ if ('where' in options) {
1060
+ validated._deprecated_where_used = true;
1061
+ }
1062
+
1063
+ return validated;
1064
+ }
1065
+
1066
+ // ============================================================================
1067
+ // COMPLETE INPUT VALIDATORS (for tool handlers)
1068
+ // ============================================================================
1069
+
1070
+ /**
1071
+ * Validate save_to_layer input (the former 'remember' wrapper used this too)
1072
+ */
1073
+ export function validateRememberInput(args) {
1074
+ if (!isPlainObject(args)) {
1075
+ throw new ValidationError('args', 'Arguments must be an object');
1076
+ }
1077
+
1078
+ return {
1079
+ content: validateContent(args.content),
1080
+ layer: validateLayer(args.layer, false),
1081
+ metadata: validateMetadata(args.metadata)
1082
+ };
1083
+ }
1084
+
1085
+ /**
1086
+ * Validate 'recall' tool input
1087
+ */
1088
+ export function validateRecallInput(args) {
1089
+ if (!isPlainObject(args)) {
1090
+ throw new ValidationError('args', 'Arguments must be an object');
1091
+ }
1092
+
1093
+ return {
1094
+ query: validateQuery(args.query),
1095
+ layer: validateLayer(args.layer, false),
1096
+ limit: validateLimit(args.limit)
1097
+ };
1098
+ }
1099
+
1100
+ /**
1101
+ * Validate 'query_layer' tool input
1102
+ */
1103
+ export function validateQueryLayerInput(args) {
1104
+ if (!isPlainObject(args)) {
1105
+ throw new ValidationError('args', 'Arguments must be an object');
1106
+ }
1107
+
1108
+ return {
1109
+ layer: validateLayer(args.layer, true), // Layer is required for query_layer
1110
+ options: validateQueryOptions(args.options)
1111
+ };
1112
+ }
1113
+
1114
+ /**
1115
+ * Validate 'save_to_layer' tool input
1116
+ */
1117
+ export function validateSaveToLayerInput(args) {
1118
+ if (!isPlainObject(args)) {
1119
+ throw new ValidationError('args', 'Arguments must be an object');
1120
+ }
1121
+
1122
+ return {
1123
+ layer: validateLayer(args.layer, true), // Layer is required
1124
+ content: validateContent(args.content),
1125
+ metadata: validateMetadata(args.metadata)
1126
+ };
1127
+ }
1128
+
1129
+ // ============================================================================
1130
+ // EXPORT DEFAULT VALIDATOR OBJECT
1131
+ // ============================================================================
1132
+
1133
+ export default {
1134
+ // Constants
1135
+ CONTENT_LIMITS,
1136
+ NUMERIC_LIMITS,
1137
+ VALID_LAYERS,
1138
+ VALID_METADATA_FIELDS,
1139
+
1140
+ // Error class
1141
+ ValidationError,
1142
+
1143
+ // Type checkers
1144
+ isString,
1145
+ isNumber,
1146
+ isInteger,
1147
+ isPlainObject,
1148
+ isArray,
1149
+
1150
+ // Content validators
1151
+ validateContent,
1152
+ validateQuery,
1153
+ validateContext,
1154
+
1155
+ // Layer validators
1156
+ validateLayer,
1157
+
1158
+ // Numeric validators
1159
+ validateNumericRange,
1160
+ validateImportance,
1161
+ validateEmotionalIntensity,
1162
+ validateLimit,
1163
+ validateTimestamp,
1164
+ validateId,
1165
+
1166
+ // Metadata validators
1167
+ validateMetadata,
1168
+ relocatedFields,
1169
+ validatePositiveInteger,
1170
+ validateTags,
1171
+ validateIdArray,
1172
+ validateStringField,
1173
+ ASSERTION_ORIGINS,
1174
+ MARKER_FIELDS,
1175
+
1176
+ // Filter/Options validators
1177
+ validateFilters,
1178
+ RECOGNISED_FILTER_KEYS,
1179
+ unrecognisedFilterKeys,
1180
+ validateQueryOptions,
1181
+
1182
+ // Complete input validators
1183
+ validateRememberInput,
1184
+ validateRecallInput,
1185
+ validateQueryLayerInput,
1186
+ validateSaveToLayerInput
1187
+ };