renor-engine 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. package/package.json +120 -4
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeErrorMessage = sanitizeErrorMessage;
4
+ exports.escapeInvisibleControls = escapeInvisibleControls;
5
+ /**
6
+ * Privacy-safe error sanitizer: redacts host filesystem paths and bounds message length.
7
+ * Preserves error descriptions, status codes, and syscalls without disclosing server directories.
8
+ */
9
+ function sanitizeErrorMessage(err) {
10
+ let message = err instanceof Error ? err.message : String(err);
11
+ if (err && typeof err === 'object' && 'path' in err && typeof err.path === 'string' && err.path.length > 0) {
12
+ message = message.split(err.path).join('[redacted-path]');
13
+ }
14
+ // Windows absolute paths: C:\...
15
+ const winPath = /(?:^|(?<=[\s(]))(['"`]?)[a-zA-Z]:\\(?:[^\s'"`:,;()\[\]\\]+\\)*[^\s'"`:,;()\[\]]*\1/g;
16
+ // Unix absolute paths: starts with / and has at least two segments (e.g. /home/user/vault or /tmp/x)
17
+ const unixPath = /(?:^|(?<=[\s(]))(['"`]?)\/(?:[^\s'"`:,;()\[\]/]+\/)+[^\s'"`:,;()\[\]]*\1/g;
18
+ return message.replace(winPath, '[redacted-path]').replace(unixPath, '[redacted-path]');
19
+ }
20
+ /**
21
+ * Characters JSON.stringify leaves literal although a reader may treat them as structure rather than
22
+ * data: the C1 controls (U+0085 NEXT LINE among them), the Unicode format controls — bidirectional
23
+ * overrides and zero-width characters a stored memory can use to reorder or hide text — and the line
24
+ * and paragraph separators. A memory is untrusted text; emitted raw, a bidirectional override makes
25
+ * the reply read backwards in an agent's transcript and a zero-width or line separator can hide an
26
+ * instruction from a human reviewer while the model still tokenizes it.
27
+ *
28
+ * Escaping them to a plain `\uXXXX` escape on the wire changes no tool's contract: JSON.stringify
29
+ * already escapes every C0 control, any JSON parser reads the same string back, so the decoded value
30
+ * is byte-identical while the raw reply an agent or a log reads shows the hidden character as an
31
+ * escape instead of applying it. Mirrors lifecycle.ts JSON_LEAVES_LITERAL, which does the same for the
32
+ * ledger's statement field. Surrogate pairs (astral format characters) are escaped unit by unit.
33
+ *
34
+ * The categories above are not the whole set a reader cannot see. Variation selectors (Mn: U+FE00–FE0F and the
35
+ * supplement U+E0100–E01EF, which spell out bytes an instruction can hide in) and blank fillers (Lo, So or Mn: the Hangul
36
+ * fillers, the braille blank, the combining grapheme joiner) are invisible in a transcript yet pass the categories
37
+ * above, so they are listed by code point (adv report renor-adv-inj6b-1012, F4). Emoji presentation selectors (U+FE0F
38
+ * after a symbol) are escaped too: still lossless after a JSON parse, a few bytes longer in the raw reply.
39
+ */
40
+ const OUTPUT_INVISIBLE = new RegExp('[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}'
41
+ + '\\u034F\\u115F\\u1160\\u2800\\u3164\\uFE00-\\uFE0F\\uFFA0\\u{E0100}-\\u{E01EF}]', 'gu');
42
+ /** A text of printable ASCII alone holds none of the characters above: found by a plain character class, not by the Unicode properties. */
43
+ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
44
+ function escapeInvisibleControls(text) {
45
+ if (PRINTABLE_ASCII.test(text))
46
+ return text;
47
+ return text.replace(OUTPUT_INVISIBLE, ch => Array.from({ length: ch.length }, (_, i) => `\\u${ch.charCodeAt(i).toString(16).padStart(4, '0')}`).join(''));
48
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.countSchemaRejections = countSchemaRejections;
4
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
5
+ const zod_compat_js_1 = require("@modelcontextprotocol/sdk/server/zod-compat.js");
6
+ const telemetry_1 = require("./telemetry");
7
+ /**
8
+ * Wraps `server` so that each call its tools' schemas refuse is counted as one `tool_rejected` event. `onRefused` hears the
9
+ * tool's name for each such call, before the event is written: a refused call is still a call (a session's first call).
10
+ */
11
+ function countSchemaRejections(server, onRefused) {
12
+ const sdk = server;
13
+ const validate = sdk.validateToolInput;
14
+ if (typeof validate !== 'function')
15
+ return;
16
+ sdk.validateToolInput = async (tool, args, toolName) => {
17
+ try {
18
+ return await validate.call(server, tool, args, toolName);
19
+ }
20
+ catch (err) {
21
+ // Inside this method the SDK throws InvalidParams for one thing only: the arguments failed the schema (an unknown or
22
+ // disabled tool is refused before it is reached). Anything else is a defect of the schema itself, not a refusal.
23
+ if (err?.code === types_js_1.ErrorCode.InvalidParams) {
24
+ onRefused?.(toolName);
25
+ await recordRejection(tool, args, toolName);
26
+ }
27
+ throw err;
28
+ }
29
+ };
30
+ }
31
+ async function recordRejection(tool, args, toolName) {
32
+ // The SDK has refused the call, so it is counted whatever happens next: a failure to say more leaves the kind at `other`.
33
+ let described = { kind: 'other' };
34
+ try {
35
+ described = await describeFailure(tool, args);
36
+ }
37
+ catch {
38
+ // Observability must never change what the caller reads.
39
+ }
40
+ (0, telemetry_1.emitTelemetry)({ event: 'tool_rejected', tool: toolName, ...described });
41
+ }
42
+ /**
43
+ * Which argument the schema refused first and how, read from the issues the SDK's own parse yields (the same function, on
44
+ * the same schema and the same arguments, so the same verdict). Issues follow the schema's own order, so "first" is stable.
45
+ */
46
+ async function describeFailure(tool, args) {
47
+ if (!tool.inputSchema)
48
+ return { kind: 'other' };
49
+ const parsed = await (0, zod_compat_js_1.safeParseAsync)(tool.inputSchema, args);
50
+ if (parsed.success)
51
+ return { kind: 'other' };
52
+ const issue = firstIssue(parsed.error);
53
+ if (!issue)
54
+ return { kind: 'other' };
55
+ // Only a name the tool's own schema declares can be an argument that failed: a key the caller invented is not one. A declared
56
+ // name that is not a plain identifier is left out too, so the call is still counted instead of the whole line being dropped.
57
+ const shape = (0, zod_compat_js_1.getObjectShape)(tool.inputSchema);
58
+ const head = issue.path[0];
59
+ const named = typeof head === 'string' && shape !== undefined && Object.prototype.hasOwnProperty.call(shape, head) && (0, telemetry_1.isSafeArgName)(head);
60
+ return { ...(named ? { arg: head } : {}), kind: kindOf(issue, args) };
61
+ }
62
+ function firstIssue(error) {
63
+ const issues = error?.issues;
64
+ if (!Array.isArray(issues))
65
+ return undefined;
66
+ const first = issues[0];
67
+ if (!first || typeof first.code !== 'string' || !Array.isArray(first.path))
68
+ return undefined;
69
+ return { code: first.code, path: first.path };
70
+ }
71
+ function kindOf(issue, args) {
72
+ switch (issue.code) {
73
+ // The schema says "invalid_type" both for an absent argument and for one of the wrong type; the call says which.
74
+ case 'invalid_type': return valueAt(args, issue.path) === undefined ? 'missing' : 'wrong_type';
75
+ case 'too_small': return 'too_small';
76
+ case 'too_big': return 'too_big';
77
+ case 'invalid_value': return 'invalid_value';
78
+ case 'invalid_format': return 'invalid_format';
79
+ default: return 'other';
80
+ }
81
+ }
82
+ /** Whatever sits at `path` in what the caller sent. Only ever compared with `undefined`: it is never kept or written. */
83
+ function valueAt(root, path) {
84
+ let value = root;
85
+ for (const key of path) {
86
+ if (value === null || typeof value !== 'object')
87
+ return undefined;
88
+ value = value[key];
89
+ }
90
+ return value;
91
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeScope = normalizeScope;
4
+ exports.dailyDirForScope = dailyDirForScope;
5
+ exports.notePathMatchesScope = notePathMatchesScope;
6
+ exports.scopeSqlPredicate = scopeSqlPredicate;
7
+ exports.scopeSpellings = scopeSpellings;
8
+ exports.savedScopeMatches = savedScopeMatches;
9
+ exports.savedScopeSqlPredicate = savedScopeSqlPredicate;
10
+ /**
11
+ * Optional memory scope for MVP tools (global vs named project/scope).
12
+ *
13
+ * Allowed forms:
14
+ * - "global" (default)
15
+ * - "scope:<slug>" or "project:<slug>"
16
+ * Slug: lowercase [a-z0-9-]{1,64} after normalization.
17
+ */
18
+ const toolRefusal_1 = require("../toolRefusal");
19
+ /** The one directory a scope owns. Read by both the JS and the SQL scope rules. */
20
+ const SCOPES_ROOT = 'Memory/Scopes/';
21
+ const SLUG_RE = /^[a-z0-9-]{1,64}$/;
22
+ /** Normalize user/agent scope input. Throws on invalid values. */
23
+ function normalizeScope(raw) {
24
+ if (raw === undefined || raw === null || raw.trim() === '')
25
+ return 'global';
26
+ const s = raw.trim().toLowerCase();
27
+ if (s === 'global')
28
+ return 'global';
29
+ let kind = null;
30
+ let slug = s;
31
+ if (s.startsWith('scope:')) {
32
+ kind = 'scope';
33
+ slug = s.slice('scope:'.length);
34
+ }
35
+ else if (s.startsWith('project:')) {
36
+ kind = 'project';
37
+ slug = s.slice('project:'.length);
38
+ }
39
+ else {
40
+ // Bare slug → scope:<slug>
41
+ kind = 'scope';
42
+ slug = s;
43
+ }
44
+ // Normalize slug: spaces/underscores → hyphens; strip invalid chars
45
+ slug = slug
46
+ .replace(/[_\s]+/g, '-')
47
+ .replace(/[^a-z0-9-]/g, '')
48
+ .replace(/-+/g, '-')
49
+ .replace(/^-|-$/g, '');
50
+ if (!SLUG_RE.test(slug)) {
51
+ // The refusal names its code for telemetry (a closed set, never built from this input); the message the
52
+ // agent reads is unchanged, and it is the agent's own argument that is quoted in it.
53
+ throw (0, toolRefusal_1.refusal)('invalid_scope', `invalid scope ${JSON.stringify(raw)}: use "global" or "scope:<slug>" / "project:<slug>" with slug matching [a-z0-9-]{1,64}`);
54
+ }
55
+ return `${kind}:${slug}`;
56
+ }
57
+ /** Daily-note directory under the vault for this scope. */
58
+ function dailyDirForScope(scope) {
59
+ if (scope === 'global')
60
+ return 'Memory/Daily';
61
+ const slug = scope.includes(':') ? scope.slice(scope.indexOf(':') + 1) : scope;
62
+ return `Memory/Scopes/${slug}/Daily`;
63
+ }
64
+ /** True if a vault-relative note path belongs to the given scope. */
65
+ function notePathMatchesScope(notePath, scope) {
66
+ const norm = notePath.replace(/\\/g, '/');
67
+ if (scope === 'global') {
68
+ // Global notes: not under Memory/Scopes/
69
+ if (norm.startsWith('Memory/Scopes/') || norm.includes('/Memory/Scopes/'))
70
+ return false;
71
+ return true;
72
+ }
73
+ const slug = scope.slice(scope.indexOf(':') + 1);
74
+ const prefix = `Memory/Scopes/${slug}/`;
75
+ return norm === prefix.slice(0, -1) || norm.startsWith(prefix) || norm.includes('/' + prefix);
76
+ }
77
+ /**
78
+ * The same rule as notePathMatchesScope, as a SQL predicate over a stored path
79
+ * column, for the callers that must filter or count rows without materializing
80
+ * every path. Each arm below is one arm of that function — the three `dir`
81
+ * terms are its equality, startsWith and includes cases — and
82
+ * tests/mvp/scope-isolation.test.ts pins them against each other, so a scope
83
+ * can never mean two different things depending on whether it was checked in
84
+ * SQL or in JS. `dir` is built from a validated slug, so it contains no LIKE
85
+ * wildcard. Bind with better-sqlite3's positional params in `params`.
86
+ */
87
+ function scopeSqlPredicate(scope, column) {
88
+ if (scope === 'global') {
89
+ return {
90
+ sql: `(${column} NOT LIKE ? AND instr(${column}, ?) = 0)`,
91
+ params: [`${SCOPES_ROOT}%`, `/${SCOPES_ROOT}`],
92
+ };
93
+ }
94
+ const dir = `${SCOPES_ROOT}${scope.slice(scope.indexOf(':') + 1)}`;
95
+ return {
96
+ sql: `(instr(${column}, ?) > 0 OR ${column} LIKE ? OR ${column} = ?)`,
97
+ params: [`/${dir}/`, `${dir}/%`, dir],
98
+ };
99
+ }
100
+ /**
101
+ * The scopes a fact or a ledger event can have been saved in and belong to `scope`: the one rule notes follow, for
102
+ * what is not a note. A named scope is its slug. `scope:<slug>`, `project:<slug>` and the bare `<slug>` are spellings
103
+ * of one scope: a note saved in any of them goes to the one folder of that slug (`dailyDirForScope`) and is read in
104
+ * all three (`notePathMatchesScope`). A fact keeps the spelling it was saved with (the ledger, its digest and the
105
+ * `scope` a read shows are what was written), and is read in every spelling of its slug, as its scope's notes are. A
106
+ * save corrects its subject and predicate in every spelling too: it closes the current value whatever spelling that
107
+ * was saved with (memory/lifecycle.ts, one lifecycle stream per slug).
108
+ * Global has one spelling. Distinct, so a caller that reads once per spelling never reads a scope twice.
109
+ */
110
+ function scopeSpellings(scope) {
111
+ if (scope === 'global')
112
+ return ['global'];
113
+ const slug = scope.slice(scope.indexOf(':') + 1);
114
+ return [`scope:${slug}`, `project:${slug}`];
115
+ }
116
+ /** Whether a fact or ledger event saved in `saved` (a stored, normalized scope) belongs to `scope`. */
117
+ function savedScopeMatches(saved, scope) {
118
+ return scopeSpellings(scope).includes(saved);
119
+ }
120
+ /**
121
+ * The same rule as savedScopeMatches, as a SQL predicate over a stored scope column (`facts.scope`,
122
+ * `fact_terms.scope`). Bind with better-sqlite3's positional params in `params`.
123
+ */
124
+ function savedScopeSqlPredicate(scope, column) {
125
+ const spellings = scopeSpellings(scope);
126
+ return { sql: `${column} IN (${spellings.map(() => '?').join(', ')})`, params: spellings };
127
+ }