@jungjaehoon/mama-core 1.9.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/db/migrations/043-add-tool-trace-failure-code.sql +11 -0
- package/dist/agent-graph/alias-write.js +3 -0
- package/dist/agent-graph/entity-resolve.js +3 -7
- package/dist/agent-graph/graph-query.js +5 -4
- package/dist/agent-graph/types.d.ts +30 -0
- package/dist/cases/store.d.ts +0 -4
- package/dist/cases/store.js +0 -66
- package/dist/cases/wiki-page-index.d.ts +0 -13
- package/dist/cases/wiki-page-index.js +0 -153
- package/dist/connectors/event-index.d.ts +1 -22
- package/dist/connectors/event-index.js +0 -157
- package/dist/context-compile/channel-grant.d.ts +46 -0
- package/dist/context-compile/channel-grant.js +66 -0
- package/dist/context-compile/compiler.js +5 -2
- package/dist/context-compile/index.d.ts +1 -0
- package/dist/context-compile/index.js +1 -0
- package/dist/context-compile/packet-store.d.ts +0 -1
- package/dist/context-compile/packet-store.js +0 -16
- package/dist/context-compile/source-readers.d.ts +1 -0
- package/dist/context-compile/source-readers.js +156 -23
- package/dist/context-compile/types.d.ts +17 -0
- package/dist/context-compile/visibility.js +28 -4
- package/dist/db-adapter/node-sqlite-adapter.js +4 -0
- package/dist/db-manager.d.ts +0 -13
- package/dist/db-manager.js +0 -66
- package/dist/decision-formatter.d.ts +0 -11
- package/dist/decision-formatter.js +0 -122
- package/dist/decision-tracker.d.ts +0 -12
- package/dist/decision-tracker.js +0 -50
- package/dist/edges/ref-validation.d.ts +1 -2
- package/dist/edges/ref-validation.js +33 -13
- package/dist/edges/store.d.ts +1 -3
- package/dist/edges/store.js +0 -214
- package/dist/edges/types.d.ts +10 -0
- package/dist/embedding-client.d.ts +0 -17
- package/dist/embedding-client.js +0 -80
- package/dist/embedding-server/mobile/auth.d.ts +1 -10
- package/dist/embedding-server/mobile/auth.js +0 -15
- package/dist/embeddings.d.ts +0 -12
- package/dist/embeddings.js +0 -68
- package/dist/entities/audit-metrics.d.ts +0 -5
- package/dist/entities/audit-metrics.js +0 -4
- package/dist/entities/candidate-generator.d.ts +1 -2
- package/dist/entities/candidate-generator.js +0 -412
- package/dist/entities/entity-list.d.ts +0 -1
- package/dist/entities/entity-list.js +0 -96
- package/dist/entities/entity-orphan-list.d.ts +0 -9
- package/dist/entities/entity-orphan-list.js +0 -88
- package/dist/entities/entity-search.d.ts +0 -1
- package/dist/entities/entity-search.js +0 -147
- package/dist/entities/errors.d.ts +0 -10
- package/dist/entities/errors.js +1 -25
- package/dist/entities/lineage-store.d.ts +0 -23
- package/dist/entities/lineage-store.js +0 -104
- package/dist/entities/policy-store.d.ts +0 -9
- package/dist/entities/policy-store.js +0 -188
- package/dist/entities/provenance-query.d.ts +0 -1
- package/dist/entities/provenance-query.js +0 -164
- package/dist/entities/resolution-engine.d.ts +1 -8
- package/dist/entities/resolution-engine.js +0 -66
- package/dist/entities/rollback-preview.d.ts +0 -4
- package/dist/entities/rollback-preview.js +0 -232
- package/dist/entities/store.d.ts +1 -10
- package/dist/entities/store.js +0 -49
- package/dist/index.d.ts +15 -31
- package/dist/index.js +4 -46
- package/dist/mama-api.d.ts +19 -1
- package/dist/mama-api.js +97 -2
- package/dist/memory/api.d.ts +8 -0
- package/dist/memory/api.js +55 -2
- package/dist/memory/event-store.d.ts +0 -1
- package/dist/memory/event-store.js +0 -15
- package/dist/memory/finding-store.d.ts +0 -1
- package/dist/memory/finding-store.js +0 -12
- package/dist/memory/scope-backfill.d.ts +0 -3
- package/dist/memory/scope-backfill.js +0 -74
- package/dist/memory/scope-store.d.ts +0 -1
- package/dist/memory/scope-store.js +0 -4
- package/dist/memory/truth-store.d.ts +0 -3
- package/dist/memory/truth-store.js +0 -22
- package/dist/memory/types.d.ts +0 -1
- package/dist/memory/types.js +0 -13
- package/dist/memory-store.d.ts +0 -16
- package/dist/memory-store.js +0 -51
- package/dist/model-runs/tool-trace-store.js +9 -5
- package/dist/model-runs/types.d.ts +3 -0
- package/dist/ollama-client.d.ts +0 -9
- package/dist/ollama-client.js +0 -125
- package/dist/outcome-tracker.d.ts +0 -14
- package/dist/outcome-tracker.js +0 -44
- package/dist/progress-indicator.d.ts +0 -4
- package/dist/progress-indicator.js +0 -7
- package/dist/provenance/source-ref.d.ts +0 -9
- package/dist/provenance/source-ref.js +0 -145
- package/dist/query-intent.d.ts +0 -4
- package/dist/query-intent.js +0 -30
- package/dist/relevance-scorer.d.ts +0 -9
- package/dist/relevance-scorer.js +0 -81
- package/dist/search/feedback-store.d.ts +0 -1
- package/dist/search/feedback-store.js +0 -4
- package/dist/search/ranker-rescore.d.ts +0 -3
- package/dist/search/ranker-rescore.js +0 -27
- package/dist/search/ranker-trainer.d.ts +0 -8
- package/dist/search/ranker-trainer.js +0 -376
- package/dist/tier-validator.d.ts +0 -12
- package/dist/tier-validator.js +0 -56
- package/package.json +1 -10
- package/dist/cases/case-links.d.ts +0 -116
- package/dist/cases/case-links.js +0 -794
- package/dist/cases/composition-overrides.d.ts +0 -90
- package/dist/cases/composition-overrides.js +0 -724
- package/dist/cases/corrections.d.ts +0 -156
- package/dist/cases/corrections.js +0 -1045
- package/dist/cases/freshness.d.ts +0 -57
- package/dist/cases/freshness.js +0 -283
- package/dist/cases/live-state.d.ts +0 -48
- package/dist/cases/live-state.js +0 -256
- package/dist/cases/membership-explain.d.ts +0 -56
- package/dist/cases/membership-explain.js +0 -140
- package/dist/cases/membership-matcher.d.ts +0 -23
- package/dist/cases/membership-matcher.js +0 -193
- package/dist/cases/merge-split.d.ts +0 -61
- package/dist/cases/merge-split.js +0 -368
- package/dist/cases/role-inference.d.ts +0 -10
- package/dist/cases/role-inference.js +0 -85
- package/dist/cases/sqlite-transaction.d.ts +0 -10
- package/dist/cases/sqlite-transaction.js +0 -62
- package/dist/cases/target-ref.d.ts +0 -25
- package/dist/cases/target-ref.js +0 -22
- package/dist/cases/tombstone-sweeper.d.ts +0 -13
- package/dist/cases/tombstone-sweeper.js +0 -128
- package/dist/entities/entity-impact.d.ts +0 -41
- package/dist/entities/entity-impact.js +0 -140
- package/dist/entities/exact-merge-backfill.d.ts +0 -11
- package/dist/entities/exact-merge-backfill.js +0 -161
- package/dist/entities/lineage-backfill.d.ts +0 -12
- package/dist/entities/lineage-backfill.js +0 -101
- package/dist/memory-inject.d.ts +0 -24
- package/dist/memory-inject.js +0 -116
|
@@ -5,32 +5,10 @@ exports.connectorEventIndexId = connectorEventIndexId;
|
|
|
5
5
|
exports.connectorEventContentHash = connectorEventContentHash;
|
|
6
6
|
exports.upsertConnectorEventIndex = upsertConnectorEventIndex;
|
|
7
7
|
exports.getConnectorEventIndexRecord = getConnectorEventIndexRecord;
|
|
8
|
-
exports.listConnectorEventsByDatetimeRange = listConnectorEventsByDatetimeRange;
|
|
9
|
-
exports.searchConnectorEventsByFTS = searchConnectorEventsByFTS;
|
|
10
8
|
exports.readConnectorCursor = readConnectorCursor;
|
|
11
|
-
exports.upsertConnectorCursor = upsertConnectorCursor;
|
|
12
9
|
exports.deleteExpiredConnectorEvents = deleteExpiredConnectorEvents;
|
|
13
|
-
exports.computeStalenessStatus = computeStalenessStatus;
|
|
14
10
|
const node_crypto_1 = require("node:crypto");
|
|
15
11
|
const canonicalize_js_1 = require("../canonicalize.js");
|
|
16
|
-
const DEFAULT_RANGE_LIMIT = 100;
|
|
17
|
-
const DEFAULT_SEARCH_LIMIT = 25;
|
|
18
|
-
const MAX_RANGE_LIMIT = 500;
|
|
19
|
-
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
20
|
-
const FIFTEEN_MINUTES_MS = 15 * 60 * 1000;
|
|
21
|
-
const SIXTY_MINUTES_MS = 60 * 60 * 1000;
|
|
22
|
-
function placeholders(values) {
|
|
23
|
-
if (values.length === 0) {
|
|
24
|
-
throw new Error('Cannot build SQL placeholders for an empty list.');
|
|
25
|
-
}
|
|
26
|
-
return values.map(() => '?').join(', ');
|
|
27
|
-
}
|
|
28
|
-
function positiveLimit(value, fallback, max = MAX_RANGE_LIMIT) {
|
|
29
|
-
if (value === undefined) {
|
|
30
|
-
return fallback;
|
|
31
|
-
}
|
|
32
|
-
return Math.min(max, Math.max(0, Math.floor(value)));
|
|
33
|
-
}
|
|
34
12
|
function nowIso() {
|
|
35
13
|
return new Date().toISOString();
|
|
36
14
|
}
|
|
@@ -119,16 +97,6 @@ function mapConnectorCursorRow(row) {
|
|
|
119
97
|
indexed_count: Number(row.indexed_count),
|
|
120
98
|
};
|
|
121
99
|
}
|
|
122
|
-
function connectorFilterSql(connectors, alias = '') {
|
|
123
|
-
if (!connectors || connectors.length === 0) {
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
126
|
-
const prefix = alias.length > 0 ? `${alias}.` : '';
|
|
127
|
-
return ` AND ${prefix}source_connector IN (${placeholders(connectors)})`;
|
|
128
|
-
}
|
|
129
|
-
function hasOwn(input, key) {
|
|
130
|
-
return Object.prototype.hasOwnProperty.call(input, key);
|
|
131
|
-
}
|
|
132
100
|
function connectorEventIndexId(sourceConnector, sourceId) {
|
|
133
101
|
const digest = (0, node_crypto_1.createHash)('sha1').update(`${sourceConnector}\0${sourceId}`, 'utf8').digest('hex');
|
|
134
102
|
return `evt_${digest.slice(0, 16)}`;
|
|
@@ -214,59 +182,6 @@ function getConnectorEventIndexRecord(adapter, sourceConnector, sourceId) {
|
|
|
214
182
|
.get(sourceConnector, sourceId);
|
|
215
183
|
return row ? mapConnectorEventIndexRow(row) : null;
|
|
216
184
|
}
|
|
217
|
-
function listConnectorEventsByDatetimeRange(adapter, input) {
|
|
218
|
-
const limit = positiveLimit(input.limit, DEFAULT_RANGE_LIMIT);
|
|
219
|
-
if (limit === 0) {
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
const order = input.order === 'desc' ? 'DESC' : 'ASC';
|
|
223
|
-
const connectors = input.connectors?.filter(Boolean) ?? [];
|
|
224
|
-
const params = [input.fromMs, input.toMs, ...connectors, limit];
|
|
225
|
-
const rows = adapter
|
|
226
|
-
.prepare(`
|
|
227
|
-
SELECT *
|
|
228
|
-
FROM connector_event_index
|
|
229
|
-
WHERE event_datetime >= ?
|
|
230
|
-
AND event_datetime <= ?
|
|
231
|
-
${connectorFilterSql(connectors)}
|
|
232
|
-
ORDER BY event_datetime ${order}, event_index_id ASC
|
|
233
|
-
LIMIT ?
|
|
234
|
-
`)
|
|
235
|
-
.all(...params);
|
|
236
|
-
return rows.map(mapConnectorEventIndexRow);
|
|
237
|
-
}
|
|
238
|
-
function searchConnectorEventsByFTS(adapter, query, options = {}) {
|
|
239
|
-
const normalizedQuery = query.trim();
|
|
240
|
-
if (normalizedQuery.length === 0) {
|
|
241
|
-
return [];
|
|
242
|
-
}
|
|
243
|
-
const limit = positiveLimit(options.limit, DEFAULT_SEARCH_LIMIT, 100);
|
|
244
|
-
if (limit === 0) {
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
const connectors = options.connectors?.filter(Boolean) ?? [];
|
|
248
|
-
const params = [normalizedQuery, ...connectors, limit];
|
|
249
|
-
const rows = adapter
|
|
250
|
-
.prepare(`
|
|
251
|
-
SELECT e.*, bm25(connector_event_index_fts) AS rank
|
|
252
|
-
FROM connector_event_index_fts
|
|
253
|
-
JOIN connector_event_index e
|
|
254
|
-
ON e.event_index_id = connector_event_index_fts.event_index_id
|
|
255
|
-
WHERE connector_event_index_fts MATCH ?
|
|
256
|
-
${connectorFilterSql(connectors, 'e')}
|
|
257
|
-
ORDER BY rank ASC, e.event_datetime DESC, e.event_index_id ASC
|
|
258
|
-
LIMIT ?
|
|
259
|
-
`)
|
|
260
|
-
.all(...params);
|
|
261
|
-
return rows.map((row) => {
|
|
262
|
-
const rank = Number(row.rank);
|
|
263
|
-
return {
|
|
264
|
-
...mapConnectorEventIndexRow(row),
|
|
265
|
-
rank,
|
|
266
|
-
score: Number.isFinite(rank) ? 1 / (1 + Math.exp(rank)) : 0,
|
|
267
|
-
};
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
185
|
function readConnectorCursor(adapter, connectorName) {
|
|
271
186
|
const row = adapter
|
|
272
187
|
.prepare(`
|
|
@@ -278,49 +193,6 @@ function readConnectorCursor(adapter, connectorName) {
|
|
|
278
193
|
.get(connectorName);
|
|
279
194
|
return row ? mapConnectorCursorRow(row) : null;
|
|
280
195
|
}
|
|
281
|
-
function upsertConnectorCursor(adapter, input) {
|
|
282
|
-
const current = readConnectorCursor(adapter, input.connector_name);
|
|
283
|
-
const merged = {
|
|
284
|
-
connector_name: input.connector_name,
|
|
285
|
-
last_seen_timestamp_ms: input.last_seen_timestamp_ms ?? current?.last_seen_timestamp_ms ?? 0,
|
|
286
|
-
last_seen_source_id: input.last_seen_source_id ?? current?.last_seen_source_id ?? '',
|
|
287
|
-
last_sweep_at: hasOwn(input, 'last_sweep_at')
|
|
288
|
-
? (input.last_sweep_at ?? null)
|
|
289
|
-
: (current?.last_sweep_at ?? null),
|
|
290
|
-
last_success_at: hasOwn(input, 'last_success_at')
|
|
291
|
-
? (input.last_success_at ?? null)
|
|
292
|
-
: (current?.last_success_at ?? null),
|
|
293
|
-
last_error: hasOwn(input, 'last_error')
|
|
294
|
-
? (input.last_error ?? null)
|
|
295
|
-
: (current?.last_error ?? null),
|
|
296
|
-
last_error_at: hasOwn(input, 'last_error_at')
|
|
297
|
-
? (input.last_error_at ?? null)
|
|
298
|
-
: (current?.last_error_at ?? null),
|
|
299
|
-
indexed_count: input.indexed_count ?? current?.indexed_count ?? 0,
|
|
300
|
-
};
|
|
301
|
-
adapter
|
|
302
|
-
.prepare(`
|
|
303
|
-
INSERT INTO connector_event_index_cursors (
|
|
304
|
-
connector_name, last_seen_timestamp_ms, last_seen_source_id, last_sweep_at,
|
|
305
|
-
last_success_at, last_error, last_error_at, indexed_count
|
|
306
|
-
)
|
|
307
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
308
|
-
ON CONFLICT(connector_name) DO UPDATE SET
|
|
309
|
-
last_seen_timestamp_ms = excluded.last_seen_timestamp_ms,
|
|
310
|
-
last_seen_source_id = excluded.last_seen_source_id,
|
|
311
|
-
last_sweep_at = excluded.last_sweep_at,
|
|
312
|
-
last_success_at = excluded.last_success_at,
|
|
313
|
-
last_error = excluded.last_error,
|
|
314
|
-
last_error_at = excluded.last_error_at,
|
|
315
|
-
indexed_count = excluded.indexed_count
|
|
316
|
-
`)
|
|
317
|
-
.run(merged.connector_name, merged.last_seen_timestamp_ms, merged.last_seen_source_id, merged.last_sweep_at, merged.last_success_at, merged.last_error, merged.last_error_at, merged.indexed_count);
|
|
318
|
-
const saved = readConnectorCursor(adapter, input.connector_name);
|
|
319
|
-
if (!saved) {
|
|
320
|
-
throw new Error(`Failed to read connector cursor after upsert: ${input.connector_name}`);
|
|
321
|
-
}
|
|
322
|
-
return saved;
|
|
323
|
-
}
|
|
324
196
|
function deleteExpiredConnectorEvents(adapter, input) {
|
|
325
197
|
if (!Number.isFinite(input.nowMs)) {
|
|
326
198
|
throw new Error('deleteExpiredConnectorEvents.nowMs must be a finite number.');
|
|
@@ -349,33 +221,4 @@ function deleteExpiredConnectorEvents(adapter, input) {
|
|
|
349
221
|
.run(cutoffMs);
|
|
350
222
|
return { rows_deleted: result.changes };
|
|
351
223
|
}
|
|
352
|
-
function computeStalenessStatus(cursor, options) {
|
|
353
|
-
if (!cursor) {
|
|
354
|
-
return 'never_swept';
|
|
355
|
-
}
|
|
356
|
-
if (cursor.last_error !== null) {
|
|
357
|
-
return 'unhealthy';
|
|
358
|
-
}
|
|
359
|
-
if (cursor.last_success_at === null) {
|
|
360
|
-
return 'never_swept';
|
|
361
|
-
}
|
|
362
|
-
const lastSuccessMs = Date.parse(cursor.last_success_at);
|
|
363
|
-
if (!Number.isFinite(lastSuccessMs)) {
|
|
364
|
-
return 'unhealthy';
|
|
365
|
-
}
|
|
366
|
-
const elapsedMs = options.nowMs - lastSuccessMs;
|
|
367
|
-
const staleAfterMs = options.staleAfterMs ?? FIVE_MINUTES_MS;
|
|
368
|
-
const warnAfterMs = options.stalenessWarnAfterMs ?? FIFTEEN_MINUTES_MS;
|
|
369
|
-
const unhealthyAfterMs = options.unhealthyAfterMs ?? SIXTY_MINUTES_MS;
|
|
370
|
-
if (elapsedMs > unhealthyAfterMs) {
|
|
371
|
-
return 'unhealthy';
|
|
372
|
-
}
|
|
373
|
-
if (elapsedMs > warnAfterMs) {
|
|
374
|
-
return 'warn';
|
|
375
|
-
}
|
|
376
|
-
if (elapsedMs > staleAfterMs) {
|
|
377
|
-
return 'stale-but-warming';
|
|
378
|
-
}
|
|
379
|
-
return 'healthy';
|
|
380
|
-
}
|
|
381
224
|
//# sourceMappingURL=event-index.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw visibility rule, in one place.
|
|
3
|
+
*
|
|
4
|
+
* There were three copies of this decision. The reader had one, twin-ref validation had a
|
|
5
|
+
* second, and the standalone citation path had a third that carried a comment promising it
|
|
6
|
+
* reproduced the reader "clause for clause" - which it did, until the reader changed. The
|
|
7
|
+
* differential test that was supposed to catch that kept passing, because its fixtures
|
|
8
|
+
* never exercised the branch production takes. Copies pinned by a test are still copies;
|
|
9
|
+
* the test just tells you later.
|
|
10
|
+
*
|
|
11
|
+
* So the rule lives here, and everything that needs it takes it from here. It has to exist
|
|
12
|
+
* in two compiled forms - a boolean for checking one row you already hold, and SQL for
|
|
13
|
+
* selecting rows you do not - and those two forms are adjacent in this file with a
|
|
14
|
+
* differential over a matrix that includes what production sends. Two forms of one rule is
|
|
15
|
+
* the irreducible minimum; three copies of a decision is not.
|
|
16
|
+
*/
|
|
17
|
+
/** Connector -> the channels of it that may be read. */
|
|
18
|
+
export type ChannelGrant = Record<string, readonly string[]>;
|
|
19
|
+
/**
|
|
20
|
+
* The rule: an event is visible when its connector is granted and its channel is one of
|
|
21
|
+
* the channels granted for that connector.
|
|
22
|
+
*
|
|
23
|
+
* A connector absent from the grant is denied. A connector present with an empty list is
|
|
24
|
+
* also denied - "granted the connector but no channel of it" is not a useful state, and
|
|
25
|
+
* reading it as "all channels" is how a grant becomes a wildcard.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isChannelGranted(connector: string, channel: string | null | undefined, grant: ChannelGrant): boolean;
|
|
28
|
+
export interface ChannelGrantClause {
|
|
29
|
+
sql: string;
|
|
30
|
+
params: unknown[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The same rule as a SQL predicate.
|
|
34
|
+
*
|
|
35
|
+
* Returns null when nothing is readable, which callers must treat as "select nothing"
|
|
36
|
+
* rather than "no filter" - an empty WHERE is how a visibility rule turns into a full scan
|
|
37
|
+
* at the exact moment it should have refused.
|
|
38
|
+
*
|
|
39
|
+
* `requestedConnectors` narrows further: a connector the caller did not ask for is not read
|
|
40
|
+
* even though it is granted. The grant is a ceiling, never an instruction.
|
|
41
|
+
*/
|
|
42
|
+
export declare function channelGrantClause(grant: ChannelGrant, requestedConnectors: readonly string[] | undefined, columns: {
|
|
43
|
+
connector: string;
|
|
44
|
+
channel: string;
|
|
45
|
+
}): ChannelGrantClause | null;
|
|
46
|
+
//# sourceMappingURL=channel-grant.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The raw visibility rule, in one place.
|
|
4
|
+
*
|
|
5
|
+
* There were three copies of this decision. The reader had one, twin-ref validation had a
|
|
6
|
+
* second, and the standalone citation path had a third that carried a comment promising it
|
|
7
|
+
* reproduced the reader "clause for clause" - which it did, until the reader changed. The
|
|
8
|
+
* differential test that was supposed to catch that kept passing, because its fixtures
|
|
9
|
+
* never exercised the branch production takes. Copies pinned by a test are still copies;
|
|
10
|
+
* the test just tells you later.
|
|
11
|
+
*
|
|
12
|
+
* So the rule lives here, and everything that needs it takes it from here. It has to exist
|
|
13
|
+
* in two compiled forms - a boolean for checking one row you already hold, and SQL for
|
|
14
|
+
* selecting rows you do not - and those two forms are adjacent in this file with a
|
|
15
|
+
* differential over a matrix that includes what production sends. Two forms of one rule is
|
|
16
|
+
* the irreducible minimum; three copies of a decision is not.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.isChannelGranted = isChannelGranted;
|
|
20
|
+
exports.channelGrantClause = channelGrantClause;
|
|
21
|
+
/**
|
|
22
|
+
* The rule: an event is visible when its connector is granted and its channel is one of
|
|
23
|
+
* the channels granted for that connector.
|
|
24
|
+
*
|
|
25
|
+
* A connector absent from the grant is denied. A connector present with an empty list is
|
|
26
|
+
* also denied - "granted the connector but no channel of it" is not a useful state, and
|
|
27
|
+
* reading it as "all channels" is how a grant becomes a wildcard.
|
|
28
|
+
*/
|
|
29
|
+
function isChannelGranted(connector, channel, grant) {
|
|
30
|
+
if (typeof channel !== 'string' || channel.length === 0)
|
|
31
|
+
return false;
|
|
32
|
+
const channels = grant[connector];
|
|
33
|
+
return Array.isArray(channels) && channels.includes(channel);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The same rule as a SQL predicate.
|
|
37
|
+
*
|
|
38
|
+
* Returns null when nothing is readable, which callers must treat as "select nothing"
|
|
39
|
+
* rather than "no filter" - an empty WHERE is how a visibility rule turns into a full scan
|
|
40
|
+
* at the exact moment it should have refused.
|
|
41
|
+
*
|
|
42
|
+
* `requestedConnectors` narrows further: a connector the caller did not ask for is not read
|
|
43
|
+
* even though it is granted. The grant is a ceiling, never an instruction.
|
|
44
|
+
*/
|
|
45
|
+
function channelGrantClause(grant, requestedConnectors, columns) {
|
|
46
|
+
const pairs = [];
|
|
47
|
+
const params = [];
|
|
48
|
+
for (const [connector, channels] of Object.entries(grant)) {
|
|
49
|
+
if (Array.isArray(requestedConnectors) && !requestedConnectors.includes(connector))
|
|
50
|
+
continue;
|
|
51
|
+
// Array.isArray, not `?? []`: the boolean form denies a malformed grant quietly, so
|
|
52
|
+
// this one must not throw on it. Two forms of a rule that disagree on bad input have
|
|
53
|
+
// already started to be two rules.
|
|
54
|
+
if (!Array.isArray(channels))
|
|
55
|
+
continue;
|
|
56
|
+
const unique = [
|
|
57
|
+
...new Set(channels.filter((channel) => typeof channel === 'string' && channel.length > 0)),
|
|
58
|
+
];
|
|
59
|
+
if (unique.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
pairs.push(`(${columns.connector} = ? AND ${columns.channel} IN (${unique.map(() => '?').join(', ')}))`);
|
|
62
|
+
params.push(connector, ...unique);
|
|
63
|
+
}
|
|
64
|
+
return pairs.length === 0 ? null : { sql: `(${pairs.join(' OR ')})`, params };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=channel-grant.js.map
|
|
@@ -152,7 +152,7 @@ function hasSeedRefVisibilityConstraints(input, boundary) {
|
|
|
152
152
|
input.range !== undefined ||
|
|
153
153
|
(input.as_of !== undefined && input.as_of !== null));
|
|
154
154
|
}
|
|
155
|
-
function assertSeedRefsVisibleToInput(adapter, input, seedRefs) {
|
|
155
|
+
function assertSeedRefsVisibleToInput(adapter, input, seedRefs, boundary) {
|
|
156
156
|
if (seedRefs.length === 0) {
|
|
157
157
|
return [];
|
|
158
158
|
}
|
|
@@ -179,6 +179,9 @@ function assertSeedRefsVisibleToInput(adapter, input, seedRefs) {
|
|
|
179
179
|
tenantId: input.tenant_id,
|
|
180
180
|
startMs: minVisibleMs(input),
|
|
181
181
|
asOfMs: maxVisibleMs(input),
|
|
182
|
+
// Without this a caller could seed a raw id from a channel the grant refuses and have
|
|
183
|
+
// its identifiers surface in the packet - a citation path reaching past the read path.
|
|
184
|
+
...(boundary?.channels ? { channels: boundary.channels } : {}),
|
|
182
185
|
});
|
|
183
186
|
for (const ref of seedRefs) {
|
|
184
187
|
if (ref.kind !== 'raw') {
|
|
@@ -278,7 +281,7 @@ async function compileContext(input, deps = {}) {
|
|
|
278
281
|
});
|
|
279
282
|
}
|
|
280
283
|
if (deps.adapter || hasSeedRefVisibilityConstraints(effectiveInput, boundary)) {
|
|
281
|
-
canonicalSeedRefs = assertSeedRefsVisibleToInput(deps.adapter, effectiveInput, seedRefs);
|
|
284
|
+
canonicalSeedRefs = assertSeedRefsVisibleToInput(deps.adapter, effectiveInput, seedRefs, boundary);
|
|
282
285
|
}
|
|
283
286
|
const canonicalScopes = (0, visibility_js_2.canonicalizeContextScopes)(effectiveInput.scopes);
|
|
284
287
|
const readInput = sourceInput(effectiveInput, boundary);
|
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./types.js"), exports);
|
|
18
18
|
__exportStar(require("./boundary-defaults.js"), exports);
|
|
19
|
+
__exportStar(require("./channel-grant.js"), exports);
|
|
19
20
|
__exportStar(require("./ref.js"), exports);
|
|
20
21
|
__exportStar(require("./visibility.js"), exports);
|
|
21
22
|
__exportStar(require("./packet-store.js"), exports);
|
|
@@ -3,7 +3,6 @@ import type { ContextPacketRecord, TrustedContextPacketLookupInput } from './typ
|
|
|
3
3
|
type ContextPacketAdapter = Pick<DatabaseAdapter, 'prepare'>;
|
|
4
4
|
export declare function insertContextPacket(adapter: ContextPacketAdapter, packet: ContextPacketRecord): ContextPacketRecord;
|
|
5
5
|
export declare function getContextPacket(adapter: ContextPacketAdapter, packetId: string): ContextPacketRecord | null;
|
|
6
|
-
export declare function listContextPacketsForModelRun(adapter: ContextPacketAdapter, modelRunId: string, limit?: number): ContextPacketRecord[];
|
|
7
6
|
export declare function getContextPacketForTrustedUse(adapter: ContextPacketAdapter, input: TrustedContextPacketLookupInput): ContextPacketRecord | null;
|
|
8
7
|
export {};
|
|
9
8
|
//# sourceMappingURL=packet-store.d.ts.map
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.insertContextPacket = insertContextPacket;
|
|
4
4
|
exports.getContextPacket = getContextPacket;
|
|
5
|
-
exports.listContextPacketsForModelRun = listContextPacketsForModelRun;
|
|
6
5
|
exports.getContextPacketForTrustedUse = getContextPacketForTrustedUse;
|
|
7
6
|
function parseJson(row, field) {
|
|
8
7
|
const value = row[field];
|
|
@@ -222,21 +221,6 @@ function getContextPacket(adapter, packetId) {
|
|
|
222
221
|
const row = adapter.prepare('SELECT * FROM context_packets WHERE packet_id = ?').get(packetId);
|
|
223
222
|
return row ? mapPacketRow(row) : null;
|
|
224
223
|
}
|
|
225
|
-
function listContextPacketsForModelRun(adapter, modelRunId, limit = 50) {
|
|
226
|
-
if (!Number.isFinite(limit) || limit <= 0) {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
const rows = adapter
|
|
230
|
-
.prepare(`
|
|
231
|
-
SELECT *
|
|
232
|
-
FROM context_packets
|
|
233
|
-
WHERE model_run_id = ?
|
|
234
|
-
ORDER BY created_at DESC, packet_id DESC
|
|
235
|
-
LIMIT ?
|
|
236
|
-
`)
|
|
237
|
-
.all(modelRunId, Math.floor(limit));
|
|
238
|
-
return rows.map((row) => mapPacketRow(row));
|
|
239
|
-
}
|
|
240
224
|
function getContextPacketForTrustedUse(adapter, input) {
|
|
241
225
|
const packet = getContextPacket(adapter, input.packetId);
|
|
242
226
|
if (!packet) {
|
|
@@ -64,6 +64,7 @@ export interface ContextSourceReaderDeps {
|
|
|
64
64
|
export declare function sourceRefsFromCandidates(candidates: readonly ContextCandidate[]): ContextRef[];
|
|
65
65
|
export declare function readMemoryCandidates(input: ContextSourceReadInput, deps?: ContextSourceReaderDeps): Promise<ContextSourceReadResult>;
|
|
66
66
|
export declare function readRawCandidates(adapter: ContextSourceAdapter, input: ContextSourceReadInput): ContextSourceReadResult;
|
|
67
|
+
export declare function setContextSourceClockForTests(clock: () => number): void;
|
|
67
68
|
export declare function readGraphCandidates(adapter: ContextSourceAdapter, input: ContextSourceReadInput, visibleRefs: readonly ContextRef[], deps?: ContextSourceReaderDeps): ContextSourceReadResult;
|
|
68
69
|
export {};
|
|
69
70
|
//# sourceMappingURL=source-readers.d.ts.map
|
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.sourceRefsFromCandidates = sourceRefsFromCandidates;
|
|
4
4
|
exports.readMemoryCandidates = readMemoryCandidates;
|
|
5
5
|
exports.readRawCandidates = readRawCandidates;
|
|
6
|
+
exports.setContextSourceClockForTests = setContextSourceClockForTests;
|
|
6
7
|
exports.readGraphCandidates = readGraphCandidates;
|
|
7
8
|
const ref_validation_js_1 = require("../edges/ref-validation.js");
|
|
8
9
|
const api_js_1 = require("../memory/api.js");
|
|
9
10
|
const ref_js_1 = require("./ref.js");
|
|
10
11
|
const visibility_js_1 = require("./visibility.js");
|
|
11
12
|
const boundary_defaults_js_1 = require("./boundary-defaults.js");
|
|
13
|
+
const channel_grant_js_1 = require("./channel-grant.js");
|
|
12
14
|
function hiddenAggregate() {
|
|
13
15
|
return { total: 0, by_kind: {}, by_reason: {} };
|
|
14
16
|
}
|
|
@@ -504,6 +506,18 @@ function readRawCandidates(adapter, input) {
|
|
|
504
506
|
if (!tableExists(adapter, 'connector_event_index')) {
|
|
505
507
|
return resultFromCandidates([], hiddenAggregate());
|
|
506
508
|
}
|
|
509
|
+
// These guards hold on BOTH paths. An explicitly emptied project window and an empty
|
|
510
|
+
// scope list are deliberate "read nothing" requests, and the grant branch skipping them
|
|
511
|
+
// would have turned two fail-closed states into reads.
|
|
512
|
+
if ((effectiveInput.connectors ?? []).length === 0 ||
|
|
513
|
+
(effectiveInput.scopes ?? []).length === 0 ||
|
|
514
|
+
hasExplicitEmptyProjectWindow(effectiveInput)) {
|
|
515
|
+
return resultFromCandidates([], hiddenAggregate());
|
|
516
|
+
}
|
|
517
|
+
const channelGrant = effectiveInput.boundary?.channels;
|
|
518
|
+
if (channelGrant) {
|
|
519
|
+
return readRawCandidatesWithinGrant(adapter, effectiveInput, channelGrant);
|
|
520
|
+
}
|
|
507
521
|
const connectors = effectiveInput.connectors ?? [];
|
|
508
522
|
const scopes = effectiveInput.scopes ?? [];
|
|
509
523
|
const projectIds = (effectiveInput.project_refs ?? []).map((project) => project.id);
|
|
@@ -512,11 +526,6 @@ function readRawCandidates(adapter, input) {
|
|
|
512
526
|
projectIds.length > 0 ||
|
|
513
527
|
Boolean(effectiveInput.tenant_id);
|
|
514
528
|
const includesLegacyGlobalSystem = hasGlobalSystemScope && !hasScopedVisibility;
|
|
515
|
-
if (connectors.length === 0 ||
|
|
516
|
-
scopes.length === 0 ||
|
|
517
|
-
hasExplicitEmptyProjectWindow(effectiveInput)) {
|
|
518
|
-
return resultFromCandidates([], hiddenAggregate());
|
|
519
|
-
}
|
|
520
529
|
const clauses = [`source_connector IN (${placeholders(connectors)})`];
|
|
521
530
|
const params = [...connectors];
|
|
522
531
|
if (projectIds.length > 0) {
|
|
@@ -571,29 +580,150 @@ function readRawCandidates(adapter, input) {
|
|
|
571
580
|
LIMIT ?
|
|
572
581
|
`)
|
|
573
582
|
.all(...params);
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
583
|
+
return resultFromCandidates(rows.map(rawRowToCandidate), hiddenAggregate());
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Raw visibility decided by the caller's (connector, channel) grant.
|
|
587
|
+
*
|
|
588
|
+
* Two conditions, both necessary: the connector is granted, and the channel is one of the
|
|
589
|
+
* channels granted for it. Nothing else gates a row - not project, not tenant, not the
|
|
590
|
+
* derived scope columns. See ContextBoundary.channels for why those were removed rather
|
|
591
|
+
* than kept alongside: measured on the live index, they hide two thirds of the corpus from
|
|
592
|
+
* any caller precise enough to name a scope, and a reader that answers narrow questions
|
|
593
|
+
* from nothing is worse than one that answers them from the events.
|
|
594
|
+
*
|
|
595
|
+
* The grant is enforced in SQL rather than filtered afterwards, so `limit` counts rows the
|
|
596
|
+
* caller may actually see. Filtering after the limit silently turns "the 200 most recent
|
|
597
|
+
* events" into "however many of the 200 most recent events happened to be readable".
|
|
598
|
+
*/
|
|
599
|
+
function readRawCandidatesWithinGrant(adapter, effectiveInput, grant) {
|
|
600
|
+
const requested = effectiveInput.connectors;
|
|
601
|
+
if (Array.isArray(requested) && requested.length === 0) {
|
|
602
|
+
return resultFromCandidates([], hiddenAggregate());
|
|
603
|
+
}
|
|
604
|
+
// The rule is compiled to SQL in exactly one place (channel-grant.ts), which is also
|
|
605
|
+
// where its boolean form lives. Building the clause here a second time is how the three
|
|
606
|
+
// copies this replaces came about.
|
|
607
|
+
const granted = (0, channel_grant_js_1.channelGrantClause)(grant, requested, {
|
|
608
|
+
connector: 'source_connector',
|
|
609
|
+
channel: 'channel',
|
|
610
|
+
});
|
|
611
|
+
// No overlap between what was asked for and what is granted reads nothing. It must not
|
|
612
|
+
// fall through to an unfiltered scan.
|
|
613
|
+
if (granted === null) {
|
|
614
|
+
return resultFromCandidates([], refusedAggregate(countRefusedByGrant(adapter, effectiveInput)));
|
|
615
|
+
}
|
|
616
|
+
const clauses = [granted.sql];
|
|
617
|
+
const params = [...granted.params];
|
|
618
|
+
const min = minVisibleTimeMs(effectiveInput);
|
|
619
|
+
if (min !== null) {
|
|
620
|
+
clauses.push('COALESCE(event_datetime, source_timestamp_ms) >= ?');
|
|
621
|
+
params.push(min);
|
|
622
|
+
}
|
|
623
|
+
// EVIDENCE IS WHAT HAS ALREADY HAPPENED, so the window ends now unless the caller named
|
|
624
|
+
// a later end explicitly.
|
|
625
|
+
//
|
|
626
|
+
// Found by running this reader against a snapshot of the live index rather than against
|
|
627
|
+
// fixtures. `event_datetime` is when the event OCCURS, and a calendar entry occurs in
|
|
628
|
+
// the future: 2,762 live rows are future-dated, one of them in 2056. Ordering by that
|
|
629
|
+
// column descending gave them the entire top-100 - a report asking what happened in the
|
|
630
|
+
// last seven days received one hundred calendar entries and zero messages. Excluding
|
|
631
|
+
// them, the same query returns 99 messages and one calendar entry.
|
|
632
|
+
//
|
|
633
|
+
// Nothing caught this before because the predicate this replaces returned no rows at
|
|
634
|
+
// all, so its ordering never mattered. A caller that genuinely wants the future still
|
|
635
|
+
// gets it by setting range.end_ms or as_of past now.
|
|
636
|
+
const max = maxVisibleTimeMs(effectiveInput) ?? nowMs();
|
|
637
|
+
clauses.push('COALESCE(event_datetime, source_timestamp_ms) <= ?');
|
|
638
|
+
params.push(max);
|
|
639
|
+
params.push(normalizeLimit(effectiveInput.limit));
|
|
640
|
+
const rows = adapter
|
|
641
|
+
.prepare(`
|
|
642
|
+
SELECT event_index_id, source_connector, source_id, channel, title, content,
|
|
643
|
+
event_datetime, source_timestamp_ms
|
|
644
|
+
FROM connector_event_index
|
|
645
|
+
WHERE ${clauses.join('\n AND ')}
|
|
646
|
+
ORDER BY COALESCE(event_datetime, source_timestamp_ms) DESC, event_index_id ASC
|
|
647
|
+
LIMIT ?
|
|
648
|
+
`)
|
|
649
|
+
.all(...params);
|
|
650
|
+
return resultFromCandidates(rows.map(rawRowToCandidate), refusedAggregate(countRefusedByGrant(adapter, effectiveInput, granted)));
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* How many events the grant kept out.
|
|
654
|
+
*
|
|
655
|
+
* Without this the caller cannot tell "this channel was quiet" from "you are not allowed to
|
|
656
|
+
* see this channel", because the filtering happens in SQL and leaves no trace. That
|
|
657
|
+
* distinction is the entire point: an owner asking about a board whose events are all
|
|
658
|
+
* refused would otherwise get a confident empty answer with a clean diagnostics block.
|
|
659
|
+
*
|
|
660
|
+
* Counted in the requested connectors only - rows of connectors the run never asked about
|
|
661
|
+
* are not "refused", they are simply not in question.
|
|
662
|
+
*/
|
|
663
|
+
function countRefusedByGrant(adapter, effectiveInput, granted = null) {
|
|
664
|
+
const requested = effectiveInput.connectors ?? [];
|
|
665
|
+
if (requested.length === 0)
|
|
666
|
+
return 0;
|
|
667
|
+
const clauses = [`source_connector IN (${placeholders(requested)})`];
|
|
668
|
+
const params = [...requested];
|
|
669
|
+
if (granted !== null) {
|
|
670
|
+
// `NOT (connector = ? AND channel IN (...))` is NULL when channel is NULL, so a
|
|
671
|
+
// NULL-channel row is neither selected nor counted - it vanishes from both sides of
|
|
672
|
+
// the answer. A connector that does not populate channel would have every event
|
|
673
|
+
// refused and the count would report zero, which is precisely the confident empty
|
|
674
|
+
// answer this count exists to prevent.
|
|
675
|
+
clauses.push(`(channel IS NULL OR NOT ${granted.sql})`);
|
|
676
|
+
params.push(...granted.params);
|
|
677
|
+
}
|
|
678
|
+
const min = minVisibleTimeMs(effectiveInput);
|
|
679
|
+
const max = maxVisibleTimeMs(effectiveInput);
|
|
680
|
+
if (min !== null) {
|
|
681
|
+
clauses.push('COALESCE(event_datetime, source_timestamp_ms) >= ?');
|
|
682
|
+
params.push(min);
|
|
683
|
+
}
|
|
684
|
+
if (max !== null) {
|
|
685
|
+
clauses.push('COALESCE(event_datetime, source_timestamp_ms) <= ?');
|
|
686
|
+
params.push(max);
|
|
687
|
+
}
|
|
688
|
+
const row = adapter
|
|
689
|
+
.prepare(`SELECT COUNT(*) AS n FROM connector_event_index WHERE ${clauses.join(' AND ')}`)
|
|
690
|
+
.get(...params);
|
|
691
|
+
return Number(row?.n ?? 0);
|
|
692
|
+
}
|
|
693
|
+
/** Injectable only so a test can pin the boundary; production reads the clock. */
|
|
694
|
+
let nowMs = () => Date.now();
|
|
695
|
+
function setContextSourceClockForTests(clock) {
|
|
696
|
+
nowMs = clock;
|
|
697
|
+
}
|
|
698
|
+
function refusedAggregate(refused) {
|
|
699
|
+
const aggregate = hiddenAggregate();
|
|
700
|
+
if (refused > 0) {
|
|
701
|
+
aggregate.total = refused;
|
|
702
|
+
aggregate.by_kind.raw = refused;
|
|
703
|
+
aggregate.by_reason.channel_not_granted = refused;
|
|
704
|
+
}
|
|
705
|
+
return aggregate;
|
|
706
|
+
}
|
|
707
|
+
function rawRowToCandidate(row) {
|
|
708
|
+
const sourceId = typeof row.source_id === 'string' && row.source_id.trim().length > 0
|
|
709
|
+
? row.source_id
|
|
710
|
+
: undefined;
|
|
711
|
+
return {
|
|
712
|
+
ref: {
|
|
579
713
|
kind: 'raw',
|
|
580
714
|
connector: String(row.source_connector),
|
|
581
715
|
raw_id: String(row.event_index_id),
|
|
582
716
|
...(sourceId ? { source_id: sourceId } : {}),
|
|
583
717
|
channel_id: typeof row.channel === 'string' ? row.channel : null,
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
support: emptySupport('connector_event_index'),
|
|
594
|
-
};
|
|
595
|
-
});
|
|
596
|
-
return resultFromCandidates(candidates, hiddenAggregate());
|
|
718
|
+
},
|
|
719
|
+
title: String(row.title ?? 'Raw event'),
|
|
720
|
+
excerpt: String(row.content ?? '').slice(0, 500),
|
|
721
|
+
score: 0.7,
|
|
722
|
+
timestamp_ms: parseTimestampMs(row.event_datetime ?? row.source_timestamp_ms),
|
|
723
|
+
source: 'raw',
|
|
724
|
+
visible: true,
|
|
725
|
+
support: emptySupport('connector_event_index'),
|
|
726
|
+
};
|
|
597
727
|
}
|
|
598
728
|
function readGraphCandidates(adapter, input, visibleRefs, deps = {}) {
|
|
599
729
|
const effectiveInput = normalizeTimeFilters(applyBoundaryDefaults(input));
|
|
@@ -633,6 +763,9 @@ function readGraphCandidates(adapter, input, visibleRefs, deps = {}) {
|
|
|
633
763
|
startMs: min,
|
|
634
764
|
asOfMs: max,
|
|
635
765
|
limit: normalizeLimit(effectiveInput.limit),
|
|
766
|
+
// Graph neighbours carry an excerpt, so an ungranted raw row reached through an edge
|
|
767
|
+
// would disclose content the reader itself refuses.
|
|
768
|
+
...(effectiveInput.boundary?.channels ? { channels: effectiveInput.boundary.channels } : {}),
|
|
636
769
|
}).filter((edge) => (min === null || edge.created_at >= min) && (max === null || edge.created_at <= max));
|
|
637
770
|
const candidates = [];
|
|
638
771
|
const emitted = new Set();
|
|
@@ -49,6 +49,23 @@ export interface ContextBoundary {
|
|
|
49
49
|
tenant_id?: string | null;
|
|
50
50
|
as_of?: string | number | null;
|
|
51
51
|
range?: ContextRange;
|
|
52
|
+
/**
|
|
53
|
+
* Which channels of each connector may be read, keyed by connector.
|
|
54
|
+
*
|
|
55
|
+
* When present this DECIDES raw visibility, and the scope/project/tenant clauses are not
|
|
56
|
+
* applied to raw rows. That is a deliberate replacement, not a relaxation, and the reason
|
|
57
|
+
* is measurable: on a live index of 30,671 events, `memory_scope_id` is populated on 37%
|
|
58
|
+
* of rows in three mutually incompatible namespaces, and 63% carry no scope at all - so a
|
|
59
|
+
* caller that names any specific scope loses exactly the two thirds of the corpus that
|
|
60
|
+
* were never labelled. A filter that hides most of the data from anyone precise enough to
|
|
61
|
+
* ask a narrow question is not a privacy boundary; it is a leak of a different kind, the
|
|
62
|
+
* kind where the system quietly answers from nothing.
|
|
63
|
+
*
|
|
64
|
+
* The grant is (connector, channel) because that is what the owner actually configures
|
|
65
|
+
* and what the connector actually emits. The caller computes it - this module enforces
|
|
66
|
+
* what it is given and never widens it.
|
|
67
|
+
*/
|
|
68
|
+
channels?: Record<string, readonly string[]>;
|
|
52
69
|
}
|
|
53
70
|
export interface CanonicalContextScopes {
|
|
54
71
|
scopes: MemoryScopeRef[];
|