@jungjaehoon/mama-core 1.8.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/agent-graph/alias-write.js +3 -0
  2. package/dist/agent-graph/entity-resolve.js +3 -7
  3. package/dist/agent-graph/graph-query.js +5 -4
  4. package/dist/agent-graph/types.d.ts +30 -0
  5. package/dist/cases/store.d.ts +0 -4
  6. package/dist/cases/store.js +0 -66
  7. package/dist/cases/wiki-page-index.d.ts +0 -13
  8. package/dist/cases/wiki-page-index.js +0 -153
  9. package/dist/connectors/event-index.d.ts +1 -22
  10. package/dist/connectors/event-index.js +0 -157
  11. package/dist/context-compile/channel-grant.d.ts +46 -0
  12. package/dist/context-compile/channel-grant.js +66 -0
  13. package/dist/context-compile/compiler.js +5 -2
  14. package/dist/context-compile/index.d.ts +1 -0
  15. package/dist/context-compile/index.js +1 -0
  16. package/dist/context-compile/packet-store.d.ts +0 -1
  17. package/dist/context-compile/packet-store.js +0 -16
  18. package/dist/context-compile/source-readers.d.ts +1 -0
  19. package/dist/context-compile/source-readers.js +156 -23
  20. package/dist/context-compile/types.d.ts +17 -0
  21. package/dist/context-compile/visibility.js +28 -4
  22. package/dist/db-adapter/node-sqlite-adapter.js +4 -0
  23. package/dist/db-manager.d.ts +0 -13
  24. package/dist/db-manager.js +0 -66
  25. package/dist/decision-formatter.d.ts +0 -11
  26. package/dist/decision-formatter.js +0 -122
  27. package/dist/decision-tracker.d.ts +0 -12
  28. package/dist/decision-tracker.js +0 -50
  29. package/dist/edges/ref-validation.d.ts +1 -2
  30. package/dist/edges/ref-validation.js +33 -13
  31. package/dist/edges/store.d.ts +1 -3
  32. package/dist/edges/store.js +0 -214
  33. package/dist/edges/types.d.ts +10 -0
  34. package/dist/embedding-client.d.ts +0 -17
  35. package/dist/embedding-client.js +0 -80
  36. package/dist/embedding-server/mobile/auth.d.ts +1 -10
  37. package/dist/embedding-server/mobile/auth.js +0 -15
  38. package/dist/embeddings.d.ts +0 -12
  39. package/dist/embeddings.js +0 -68
  40. package/dist/entities/audit-metrics.d.ts +0 -5
  41. package/dist/entities/audit-metrics.js +0 -4
  42. package/dist/entities/candidate-generator.d.ts +1 -2
  43. package/dist/entities/candidate-generator.js +0 -412
  44. package/dist/entities/entity-list.d.ts +0 -1
  45. package/dist/entities/entity-list.js +0 -96
  46. package/dist/entities/entity-orphan-list.d.ts +0 -9
  47. package/dist/entities/entity-orphan-list.js +0 -88
  48. package/dist/entities/entity-search.d.ts +0 -1
  49. package/dist/entities/entity-search.js +0 -147
  50. package/dist/entities/errors.d.ts +0 -10
  51. package/dist/entities/errors.js +1 -25
  52. package/dist/entities/lineage-store.d.ts +0 -23
  53. package/dist/entities/lineage-store.js +0 -104
  54. package/dist/entities/policy-store.d.ts +0 -9
  55. package/dist/entities/policy-store.js +0 -188
  56. package/dist/entities/provenance-query.d.ts +0 -1
  57. package/dist/entities/provenance-query.js +0 -164
  58. package/dist/entities/resolution-engine.d.ts +1 -8
  59. package/dist/entities/resolution-engine.js +0 -66
  60. package/dist/entities/rollback-preview.d.ts +0 -4
  61. package/dist/entities/rollback-preview.js +0 -232
  62. package/dist/entities/store.d.ts +1 -10
  63. package/dist/entities/store.js +0 -49
  64. package/dist/index.d.ts +15 -31
  65. package/dist/index.js +4 -46
  66. package/dist/mama-api.d.ts +19 -1
  67. package/dist/mama-api.js +97 -2
  68. package/dist/memory/api.d.ts +9 -0
  69. package/dist/memory/api.js +68 -4
  70. package/dist/memory/event-store.d.ts +0 -1
  71. package/dist/memory/event-store.js +0 -15
  72. package/dist/memory/finding-store.d.ts +0 -1
  73. package/dist/memory/finding-store.js +0 -12
  74. package/dist/memory/scope-backfill.d.ts +0 -3
  75. package/dist/memory/scope-backfill.js +0 -74
  76. package/dist/memory/scope-store.d.ts +0 -1
  77. package/dist/memory/scope-store.js +0 -4
  78. package/dist/memory/truth-store.d.ts +0 -3
  79. package/dist/memory/truth-store.js +0 -22
  80. package/dist/memory/types.d.ts +0 -1
  81. package/dist/memory/types.js +0 -13
  82. package/dist/memory-store.d.ts +0 -16
  83. package/dist/memory-store.js +0 -51
  84. package/dist/ollama-client.d.ts +0 -9
  85. package/dist/ollama-client.js +0 -125
  86. package/dist/outcome-tracker.d.ts +0 -14
  87. package/dist/outcome-tracker.js +0 -44
  88. package/dist/progress-indicator.d.ts +0 -4
  89. package/dist/progress-indicator.js +0 -7
  90. package/dist/provenance/source-ref.d.ts +0 -9
  91. package/dist/provenance/source-ref.js +0 -145
  92. package/dist/query-intent.d.ts +0 -4
  93. package/dist/query-intent.js +0 -30
  94. package/dist/relevance-scorer.d.ts +0 -9
  95. package/dist/relevance-scorer.js +0 -81
  96. package/dist/search/feedback-store.d.ts +0 -1
  97. package/dist/search/feedback-store.js +0 -4
  98. package/dist/search/ranker-rescore.d.ts +0 -3
  99. package/dist/search/ranker-rescore.js +0 -27
  100. package/dist/search/ranker-trainer.d.ts +0 -8
  101. package/dist/search/ranker-trainer.js +0 -376
  102. package/dist/tier-validator.d.ts +0 -12
  103. package/dist/tier-validator.js +0 -56
  104. package/package.json +1 -10
  105. package/dist/cases/case-links.d.ts +0 -116
  106. package/dist/cases/case-links.js +0 -794
  107. package/dist/cases/composition-overrides.d.ts +0 -90
  108. package/dist/cases/composition-overrides.js +0 -724
  109. package/dist/cases/corrections.d.ts +0 -156
  110. package/dist/cases/corrections.js +0 -1045
  111. package/dist/cases/freshness.d.ts +0 -57
  112. package/dist/cases/freshness.js +0 -283
  113. package/dist/cases/live-state.d.ts +0 -48
  114. package/dist/cases/live-state.js +0 -256
  115. package/dist/cases/membership-explain.d.ts +0 -56
  116. package/dist/cases/membership-explain.js +0 -140
  117. package/dist/cases/membership-matcher.d.ts +0 -23
  118. package/dist/cases/membership-matcher.js +0 -193
  119. package/dist/cases/merge-split.d.ts +0 -61
  120. package/dist/cases/merge-split.js +0 -368
  121. package/dist/cases/role-inference.d.ts +0 -10
  122. package/dist/cases/role-inference.js +0 -85
  123. package/dist/cases/sqlite-transaction.d.ts +0 -10
  124. package/dist/cases/sqlite-transaction.js +0 -62
  125. package/dist/cases/target-ref.d.ts +0 -25
  126. package/dist/cases/target-ref.js +0 -22
  127. package/dist/cases/tombstone-sweeper.d.ts +0 -13
  128. package/dist/cases/tombstone-sweeper.js +0 -128
  129. package/dist/entities/entity-impact.d.ts +0 -41
  130. package/dist/entities/entity-impact.js +0 -140
  131. package/dist/entities/exact-merge-backfill.d.ts +0 -11
  132. package/dist/entities/exact-merge-backfill.js +0 -161
  133. package/dist/entities/lineage-backfill.d.ts +0 -12
  134. package/dist/entities/lineage-backfill.js +0 -101
  135. package/dist/memory-inject.d.ts +0 -24
  136. package/dist/memory-inject.js +0 -116
@@ -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);
@@ -1,5 +1,6 @@
1
1
  export * from './types.js';
2
2
  export * from './boundary-defaults.js';
3
+ export * from './channel-grant.js';
3
4
  export * from './ref.js';
4
5
  export * from './visibility.js';
5
6
  export * from './packet-store.js';
@@ -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
- const candidates = rows.map((row) => {
575
- const sourceId = typeof row.source_id === 'string' && row.source_id.trim().length > 0
576
- ? row.source_id
577
- : undefined;
578
- const ref = {
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
- return {
586
- ref,
587
- title: String(row.title ?? 'Raw event'),
588
- excerpt: String(row.content ?? '').slice(0, 500),
589
- score: 0.7,
590
- timestamp_ms: parseTimestampMs(row.event_datetime ?? row.source_timestamp_ms),
591
- source: 'raw',
592
- visible: true,
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[];
@@ -8,6 +8,7 @@ exports.normalizeSeedRefs = normalizeSeedRefs;
8
8
  const node_crypto_1 = require("node:crypto");
9
9
  const types_js_1 = require("../memory/types.js");
10
10
  const ref_js_1 = require("./ref.js");
11
+ const channel_grant_js_1 = require("./channel-grant.js");
11
12
  const SCOPE_ORDER = {
12
13
  project: 0,
13
14
  channel: 1,
@@ -110,13 +111,36 @@ function assertContextBoundaryAllowsInput(input) {
110
111
  throw new Error('Requested tenant is outside the context boundary');
111
112
  }
112
113
  const seedRefs = (0, ref_js_1.normalizeContextRefs)(input.seedRefs);
113
- assertSeedRefsAllowed(seedRefs, boundaryConnectors, hasConnectorBoundary);
114
- }
115
- function assertSeedRefsAllowed(seedRefs, boundaryConnectors, hasConnectorBoundary) {
114
+ assertSeedRefsAllowed(seedRefs, boundaryConnectors, hasConnectorBoundary, input.boundary.channels);
115
+ }
116
+ /**
117
+ * A seed ref must satisfy the same visibility rule as a row the reader would have returned.
118
+ *
119
+ * The connector check alone was never enough, and once raw visibility is decided by
120
+ * (connector, channel) it is plainly not: a caller could name the raw id of an event in an
121
+ * ungranted channel and have its identifiers - upstream source id and channel - come back
122
+ * in the packet's source_refs, which is then persisted. That is a citation path reaching
123
+ * past the reading path, the exact asymmetry this rebuild exists to remove.
124
+ *
125
+ * The channel is checked only when the boundary carries a grant. Without one there is no
126
+ * channel rule to enforce and the connector check stands, unchanged.
127
+ */
128
+ function assertSeedRefsAllowed(seedRefs, boundaryConnectors, hasConnectorBoundary, channelGrant) {
116
129
  for (const ref of seedRefs) {
117
- if (ref.kind === 'raw' && hasConnectorBoundary && !boundaryConnectors.has(ref.connector)) {
130
+ if (ref.kind !== 'raw')
131
+ continue;
132
+ if (hasConnectorBoundary && !boundaryConnectors.has(ref.connector)) {
118
133
  throw new Error(`Seed raw ref connector is outside the context boundary: ${ref.connector}`);
119
134
  }
135
+ if (!channelGrant)
136
+ continue;
137
+ // Calls the shared rule rather than re-deriving membership. An inline re-derivation
138
+ // here already disagreed with it on the empty-channel case, which is how a fourth copy
139
+ // of a decision starts: not by being written as a copy, but by being written as
140
+ // "obviously the same check".
141
+ if (!(0, channel_grant_js_1.isChannelGranted)(ref.connector, ref.channel_id, channelGrant)) {
142
+ throw new Error(`Seed raw ref channel is outside the context boundary: ${ref.connector}`);
143
+ }
120
144
  }
121
145
  }
122
146
  function isRecord(value) {
@@ -613,6 +613,10 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
613
613
  }
614
614
  }
615
615
  this.exec(`
616
+ CREATE INDEX IF NOT EXISTS idx_decisions_topic
617
+ ON decisions(topic)
618
+ `);
619
+ this.exec(`
616
620
  CREATE INDEX IF NOT EXISTS idx_decisions_envelope_hash
617
621
  ON decisions(envelope_hash)
618
622
  `);
@@ -147,11 +147,6 @@ export declare function getAdapter(): DatabaseAdapter;
147
147
  */
148
148
  export declare function closeDB(): Promise<void>;
149
149
  export declare function ensureMemoryScope(kind: string, externalId: string): Promise<string>;
150
- export declare function bindMemoryToScope(memoryId: string, scopeId: string, isPrimary?: boolean): Promise<void>;
151
- export declare function listScopesForMemory(memoryId: string): Promise<Array<{
152
- kind: string;
153
- id: string;
154
- }>>;
155
150
  /**
156
151
  * Insert embedding into vector search table
157
152
  *
@@ -301,12 +296,4 @@ export declare function fts5Search(query: string, limit?: number): Promise<{
301
296
  id: string;
302
297
  rank: number;
303
298
  }[]>;
304
- /**
305
- * Re-generate all embeddings using the currently configured model.
306
- * Call this after changing the embedding model in config.json.
307
- *
308
- * @param onProgress - Optional callback with (completed, total) counts
309
- * @returns Number of embeddings regenerated
310
- */
311
- export declare function reindexEmbeddings(onProgress?: (completed: number, total: number) => void): Promise<number>;
312
299
  //# sourceMappingURL=db-manager.d.ts.map
@@ -29,8 +29,6 @@ exports.getDB = getDB;
29
29
  exports.getAdapter = getAdapter;
30
30
  exports.closeDB = closeDB;
31
31
  exports.ensureMemoryScope = ensureMemoryScope;
32
- exports.bindMemoryToScope = bindMemoryToScope;
33
- exports.listScopesForMemory = listScopesForMemory;
34
32
  exports.insertEmbedding = insertEmbedding;
35
33
  exports.vectorSearch = vectorSearch;
36
34
  exports.insertDecisionWithEmbedding = insertDecisionWithEmbedding;
@@ -43,7 +41,6 @@ exports.getDbPath = getDbPath;
43
41
  exports.resetDBState = resetDBState;
44
42
  exports.isTestMode = isTestMode;
45
43
  exports.fts5Search = fts5Search;
46
- exports.reindexEmbeddings = reindexEmbeddings;
47
44
  const path_1 = __importDefault(require("path"));
48
45
  const os_1 = __importDefault(require("os"));
49
46
  const debug_logger_js_1 = require("./debug-logger.js");
@@ -262,27 +259,6 @@ async function ensureMemoryScope(kind, externalId) {
262
259
  .run(id, kind, externalId);
263
260
  return id;
264
261
  }
265
- async function bindMemoryToScope(memoryId, scopeId, isPrimary = false) {
266
- const adapter = getAdapter();
267
- adapter
268
- .prepare(`
269
- INSERT OR REPLACE INTO memory_scope_bindings (memory_id, scope_id, is_primary)
270
- VALUES (?, ?, ?)
271
- `)
272
- .run(memoryId, scopeId, isPrimary ? 1 : 0);
273
- }
274
- async function listScopesForMemory(memoryId) {
275
- const adapter = getAdapter();
276
- return adapter
277
- .prepare(`
278
- SELECT ms.kind, ms.external_id AS id
279
- FROM memory_scope_bindings msb
280
- JOIN memory_scopes ms ON ms.id = msb.scope_id
281
- WHERE msb.memory_id = ?
282
- ORDER BY msb.is_primary DESC, ms.created_at ASC
283
- `)
284
- .all(memoryId);
285
- }
286
262
  /**
287
263
  * Insert embedding into vector search table
288
264
  *
@@ -811,46 +787,4 @@ async function fts5Search(query, limit = 10) {
811
787
  `);
812
788
  return stmt.all(query, limit);
813
789
  }
814
- /**
815
- * Re-generate all embeddings using the currently configured model.
816
- * Call this after changing the embedding model in config.json.
817
- *
818
- * @param onProgress - Optional callback with (completed, total) counts
819
- * @returns Number of embeddings regenerated
820
- */
821
- async function reindexEmbeddings(onProgress) {
822
- const adapter = getAdapter();
823
- const { generateEnhancedEmbedding } = await import('./embeddings.js');
824
- const decisions = adapter
825
- .prepare('SELECT rowid, topic, decision, reasoning, outcome, confidence FROM decisions')
826
- .all();
827
- if (typeof adapter.insertEmbedding !== 'function') {
828
- throw new Error('Embedding insertion is not available on this adapter');
829
- }
830
- const total = decisions.length;
831
- let completed = 0;
832
- for (const row of decisions) {
833
- try {
834
- // e5 passage role (default) - stored vector
835
- const embedding = await generateEnhancedEmbedding({
836
- topic: row.topic,
837
- decision: row.decision,
838
- reasoning: row.reasoning || undefined,
839
- outcome: row.outcome || undefined,
840
- confidence: row.confidence ?? undefined,
841
- });
842
- adapter.insertEmbedding(row.rowid, embedding);
843
- completed++;
844
- if (onProgress && completed % 50 === 0) {
845
- onProgress(completed, total);
846
- }
847
- }
848
- catch (e) {
849
- (0, debug_logger_js_1.error)(`[db-manager] Failed to reindex rowid ${row.rowid}: ${e}`);
850
- }
851
- }
852
- onProgress?.(completed, total);
853
- (0, debug_logger_js_1.info)(`[db-manager] Reindexed ${completed}/${total} embeddings`);
854
- return completed;
855
- }
856
790
  //# sourceMappingURL=db-manager.js.map
@@ -140,13 +140,6 @@ export declare function ensureTokenBudget(text: string, maxTokens: number): stri
140
140
  * Heuristic: ~1 token per 4 characters
141
141
  */
142
142
  export declare function estimateTokens(text: string): number;
143
- /**
144
- * Format context in Claude-friendly Instant Answer format
145
- *
146
- * Story 014.7.10: Claude-Friendly Context Formatting
147
- * AC #1: Instant Answer format with trust components
148
- */
149
- export declare function formatInstantAnswer(decision: DecisionForFormat | null, options?: FormatOptions): string | null;
150
143
  /**
151
144
  * Extract quick answer from decision
152
145
  */
@@ -161,10 +154,6 @@ export declare function extractCodeExample(decision: DecisionForFormat): string
161
154
  * Story 014.7.10 AC #2: Trust Context display
162
155
  */
163
156
  export declare function formatTrustContext(trustCtx: TrustContext | null | undefined): string | null;
164
- /**
165
- * Format decision as curiosity-inducing teaser
166
- */
167
- export declare function formatTeaser(decision: DecisionForFormat | null): string | null;
168
157
  /**
169
158
  * Format mama.recall() results in readable format
170
159
  */