fuli-context 0.5.0 → 0.7.2

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 (206) hide show
  1. package/README.en.md +2 -333
  2. package/README.md +370 -202
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -5
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +51 -28
  59. package/src/cli/local-runtime-options.js +14 -8
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +1 -1
  92. package/src/local-runtime/lifecycle.js +26 -15
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server.js +36 -17
  100. package/src/setup/agent-installation-status.js +2 -1
  101. package/src/setup/agents.js +2 -1
  102. package/src/setup/claude-code-config.js +2 -1
  103. package/src/setup/codex-bootstrap.js +12 -7
  104. package/src/setup/container-runtime.js +25 -21
  105. package/src/setup/graph-runtime.js +148 -56
  106. package/src/setup/node-runtime.js +3 -3
  107. package/src/setup/options.js +2 -4
  108. package/src/setup/paths.js +18 -3
  109. package/src/setup/review-skill-adapters.js +49 -0
  110. package/src/setup/setup.js +26 -2
  111. package/src/setup/uninstall.js +4 -2
  112. package/src/system/resource-monitor.js +317 -0
  113. package/src/system/runtime-settings.js +131 -0
  114. package/src/system/system-service.js +119 -0
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. package/src/store.js +0 -2
@@ -1,113 +0,0 @@
1
- CREATE TABLE schema_migrations_new (
2
- version INTEGER PRIMARY KEY,
3
- checksum TEXT NOT NULL,
4
- applied_at TEXT NOT NULL
5
- );
6
-
7
- INSERT INTO schema_migrations_new (version, checksum, applied_at)
8
- SELECT
9
- version,
10
- CASE version
11
- WHEN 1 THEN 'ab393d736d8ff18fde75478002b2b4b3b4d7e906974cd724058e31436fba0ea9'
12
- END,
13
- applied_at
14
- FROM schema_migrations;
15
-
16
- DROP TABLE schema_migrations;
17
- ALTER TABLE schema_migrations_new RENAME TO schema_migrations;
18
-
19
- CREATE TABLE facts_new (
20
- id TEXT PRIMARY KEY,
21
- space_id TEXT NOT NULL REFERENCES spaces(id),
22
- subject TEXT NOT NULL,
23
- predicate TEXT NOT NULL,
24
- object TEXT NOT NULL,
25
- source_episode_id TEXT NOT NULL REFERENCES episodes(id),
26
- status TEXT NOT NULL CHECK (
27
- status IN ('confirmed', 'observed', 'suggested', 'rejected', 'deprecated')
28
- ),
29
- confidence REAL NOT NULL DEFAULT 1.0 CHECK (
30
- typeof(confidence) IN ('integer', 'real') AND confidence >= 0.0 AND confidence <= 1.0
31
- ),
32
- sensitivity TEXT NOT NULL DEFAULT 'normal' CHECK (
33
- sensitivity IN ('normal', 'private', 'restricted')
34
- ),
35
- scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('personal', 'public')),
36
- valid_at TEXT NOT NULL,
37
- invalid_at TEXT,
38
- replaced_by_fact_id TEXT REFERENCES facts_new(id)
39
- );
40
-
41
- INSERT INTO facts_new (
42
- id, space_id, subject, predicate, object, source_episode_id, status,
43
- confidence, sensitivity, scope, valid_at, invalid_at, replaced_by_fact_id
44
- )
45
- SELECT
46
- id, space_id, subject, predicate, object, source_episode_id, status,
47
- confidence, sensitivity, scope, valid_at, invalid_at, NULL
48
- FROM facts;
49
-
50
- UPDATE facts_new
51
- SET replaced_by_fact_id = (
52
- SELECT facts.replaced_by_fact_id
53
- FROM facts
54
- WHERE facts.id = facts_new.id
55
- );
56
-
57
- DROP TABLE facts;
58
- ALTER TABLE facts_new RENAME TO facts;
59
- CREATE INDEX facts_current_idx ON facts(space_id, predicate, invalid_at);
60
- CREATE INDEX facts_source_idx ON facts(source_episode_id);
61
-
62
- CREATE TABLE candidates_new (
63
- id TEXT PRIMARY KEY,
64
- personal_space_id TEXT NOT NULL REFERENCES spaces(id),
65
- target_space_id TEXT REFERENCES spaces(id),
66
- episode_id TEXT NOT NULL REFERENCES episodes(id),
67
- reason TEXT NOT NULL,
68
- status TEXT NOT NULL CHECK (
69
- status IN ('pending', 'synced', 'personal_only', 'ignored')
70
- ),
71
- created_at TEXT NOT NULL,
72
- decided_at TEXT
73
- );
74
-
75
- INSERT INTO candidates_new (
76
- id, personal_space_id, target_space_id, episode_id, reason,
77
- status, created_at, decided_at
78
- )
79
- SELECT
80
- id, personal_space_id, target_space_id, episode_id, reason,
81
- status, created_at, decided_at
82
- FROM candidates;
83
-
84
- DROP TABLE candidates;
85
- ALTER TABLE candidates_new RENAME TO candidates;
86
-
87
- CREATE TABLE outbox_new (
88
- id TEXT PRIMARY KEY,
89
- kind TEXT NOT NULL,
90
- aggregate_id TEXT NOT NULL,
91
- payload_json TEXT NOT NULL,
92
- status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent')),
93
- attempts INTEGER NOT NULL DEFAULT 0 CHECK (
94
- typeof(attempts) = 'integer' AND attempts >= 0
95
- ),
96
- next_attempt_at TEXT,
97
- created_at TEXT NOT NULL,
98
- sent_at TEXT,
99
- last_error TEXT
100
- );
101
-
102
- INSERT INTO outbox_new (
103
- id, kind, aggregate_id, payload_json, status, attempts,
104
- next_attempt_at, created_at, sent_at, last_error
105
- )
106
- SELECT
107
- id, kind, aggregate_id, payload_json, status, attempts,
108
- next_attempt_at, created_at, sent_at, last_error
109
- FROM outbox;
110
-
111
- DROP TABLE outbox;
112
- ALTER TABLE outbox_new RENAME TO outbox;
113
- CREATE INDEX outbox_pending_idx ON outbox(status, next_attempt_at, created_at);
@@ -1,46 +0,0 @@
1
- import { CandidateStatus, FactScope, FactStatus, Sensitivity } from '../models.js';
2
-
3
- const FACT_STATUSES = new Set(Object.values(FactStatus));
4
- const CANDIDATE_STATUSES = new Set(Object.values(CandidateStatus));
5
- const SENSITIVITIES = new Set(Object.values(Sensitivity));
6
- const SCOPES = new Set(Object.values(FactScope));
7
- const OUTBOX_STATUSES = new Set(['pending', 'sent']);
8
-
9
- export function validateFactRecord(fact) {
10
- requireMember('fact status', fact.status, FACT_STATUSES);
11
- if (
12
- typeof fact.confidence !== 'number' ||
13
- !Number.isFinite(fact.confidence) ||
14
- fact.confidence < 0 ||
15
- fact.confidence > 1
16
- ) {
17
- throw new TypeError('Fact confidence must be between 0 and 1');
18
- }
19
- requireMember('fact sensitivity', fact.sensitivity, SENSITIVITIES);
20
- requireMember('fact scope', fact.scope, SCOPES);
21
- return fact;
22
- }
23
-
24
- export function validateCandidateRecord(candidate) {
25
- requireMember('candidate status', candidate.status, CANDIDATE_STATUSES);
26
- return candidate;
27
- }
28
-
29
- export function validateOutboxRecord(entry) {
30
- requireMember('outbox status', entry.status, OUTBOX_STATUSES);
31
- if (!Number.isInteger(entry.attempts) || entry.attempts < 0) {
32
- throw new TypeError('Outbox attempts must be a nonnegative integer');
33
- }
34
- return entry;
35
- }
36
-
37
- export function validateSnapshotRecords(snapshot) {
38
- for (const fact of snapshot.facts) validateFactRecord(fact);
39
- for (const candidate of snapshot.candidates) validateCandidateRecord(candidate);
40
- for (const entry of snapshot.outbox) validateOutboxRecord(entry);
41
- return snapshot;
42
- }
43
-
44
- function requireMember(label, value, allowed) {
45
- if (!allowed.has(value)) throw new TypeError(`Invalid ${label}: ${value}`);
46
- }
@@ -1,54 +0,0 @@
1
- import { CandidateStatus, FactStatus, nowIso } from '../models.js';
2
- import { validateSnapshotRecords } from './record-validation.js';
3
-
4
- export function normalizeSnapshot(snapshot = {}, { outbox = [], imports = [] } = {}) {
5
- const input = cloneSnapshot(snapshot);
6
- const normalized = {
7
- spaces: (input.spaces ?? []).map((space) => ({
8
- ...space,
9
- description: space.description ?? null,
10
- createdAt: space.createdAt ?? nowIso()
11
- })),
12
- subscriptions: (input.subscriptions ?? []).map((subscription) => ({
13
- ...subscription,
14
- mode: subscription.mode ?? 'latest',
15
- createdAt: subscription.createdAt ?? nowIso()
16
- })),
17
- episodes: (input.episodes ?? []).map((episode) => ({
18
- ...episode,
19
- sourceUri: episode.sourceUri ?? null,
20
- metadata: episode.metadata ?? {},
21
- createdAt: episode.createdAt ?? nowIso()
22
- })),
23
- facts: (input.facts ?? []).map((fact) => ({
24
- ...fact,
25
- status: fact.status ?? FactStatus.CONFIRMED,
26
- confidence: fact.confidence ?? 1,
27
- sensitivity: fact.sensitivity ?? 'normal',
28
- scope: fact.scope ?? 'personal',
29
- validAt: fact.validAt ?? nowIso(),
30
- invalidAt: fact.invalidAt ?? null,
31
- replacedByFactId: fact.replacedByFactId ?? null
32
- })),
33
- candidates: (input.candidates ?? []).map(normalizeCandidate),
34
- outbox: Object.hasOwn(input, 'outbox') ? input.outbox : cloneSnapshot(outbox),
35
- imports: Object.hasOwn(input, 'imports') ? input.imports : cloneSnapshot(imports)
36
- };
37
- return validateSnapshotRecords(normalized);
38
- }
39
-
40
- export function cloneSnapshot(value) {
41
- return JSON.parse(JSON.stringify(value));
42
- }
43
-
44
- function normalizeCandidate(candidate) {
45
- const status = candidate.status ?? CandidateStatus.PENDING;
46
- const createdAt = candidate.createdAt ?? nowIso();
47
- return {
48
- ...candidate,
49
- targetSpaceId: candidate.targetSpaceId ?? null,
50
- status,
51
- createdAt,
52
- decidedAt: candidate.decidedAt ?? (status === CandidateStatus.PENDING ? null : createdAt)
53
- };
54
- }
@@ -1,84 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
-
3
- import { CandidateStatus, nowIso } from '../../models.js';
4
- import { validateCandidateRecord } from '../record-validation.js';
5
- import { mapCandidate } from './mapper.js';
6
-
7
- export class CandidatesRepository {
8
- constructor(db) {
9
- this.insertStatement = db.prepare(`
10
- INSERT INTO candidates (
11
- id, personal_space_id, target_space_id, episode_id, reason,
12
- status, created_at, decided_at
13
- ) VALUES (
14
- @id, @personalSpaceId, @targetSpaceId, @episodeId, @reason,
15
- @status, @createdAt, @decidedAt
16
- )
17
- `);
18
- this.getStatement = db.prepare('SELECT * FROM candidates WHERE id = ?');
19
- this.listStatement = db.prepare('SELECT * FROM candidates ORDER BY rowid');
20
- this.pendingStatement = db.prepare(`
21
- SELECT * FROM candidates
22
- WHERE personal_space_id = ? AND status = 'pending'
23
- ORDER BY rowid
24
- `);
25
- this.updateStatusStatement = db.prepare(`
26
- UPDATE candidates SET status = ?, decided_at = ? WHERE id = ?
27
- `);
28
- this.deleteStatement = db.prepare('DELETE FROM candidates');
29
- }
30
-
31
- add(candidate) {
32
- if (candidate.id && this.get(candidate.id)) {
33
- throw new Error(`Duplicate candidate id: ${candidate.id}`);
34
- }
35
-
36
- const status = candidate.status ?? CandidateStatus.PENDING;
37
- const createdAt = candidate.createdAt ?? nowIso();
38
- const stored = {
39
- id: candidate.id || randomUUID(),
40
- personalSpaceId: candidate.personalSpaceId,
41
- targetSpaceId: candidate.targetSpaceId ?? null,
42
- episodeId: candidate.episodeId,
43
- reason: candidate.reason,
44
- status,
45
- createdAt,
46
- decidedAt: candidate.decidedAt ?? (
47
- status === CandidateStatus.PENDING ? null : createdAt
48
- )
49
- };
50
- validateCandidateRecord(stored);
51
- this.insertStatement.run(stored);
52
- return this.get(stored.id);
53
- }
54
-
55
- get(id) {
56
- return mapCandidate(this.getStatement.get(id));
57
- }
58
-
59
- list() {
60
- return this.listStatement.all().map(mapCandidate);
61
- }
62
-
63
- pending(personalSpaceId) {
64
- return this.pendingStatement.all(personalSpaceId).map(mapCandidate);
65
- }
66
-
67
- updateStatus(id, status) {
68
- const candidate = this.get(id);
69
- if (!candidate) throw new Error(`Candidate not found: ${id}`);
70
- const decidedAt = status === CandidateStatus.PENDING ? null : nowIso();
71
- validateCandidateRecord({ ...candidate, status, decidedAt });
72
- this.updateStatusStatement.run(status, decidedAt, id);
73
- return this.get(id);
74
- }
75
-
76
- insertSnapshot(candidate) {
77
- validateCandidateRecord(candidate);
78
- this.insertStatement.run(candidate);
79
- }
80
-
81
- deleteAll() {
82
- this.deleteStatement.run();
83
- }
84
- }
@@ -1,154 +0,0 @@
1
- import {
2
- MAX_EPISODE_METADATA_BYTES,
3
- MAX_EPISODE_METADATA_FIELD_BYTES,
4
- normalizeCorrectionEvidencePreviewOptions,
5
- normalizeEvidencePreviewOptions,
6
- previewEpisode
7
- } from '../episode-evidence-preview.js';
8
- import { sqliteEpisodeRestrictedSql } from './episode-privacy-sql.js';
9
-
10
- export class EpisodeEvidenceRepository {
11
- constructor(db) {
12
- this.db = db;
13
- this.correctionStatements = new Map();
14
- this.sourceStatement = db.prepare(sourceQuery());
15
- }
16
-
17
- source(spaceId, episodeId, options = {}) {
18
- const normalized = normalizeEvidencePreviewOptions(options);
19
- const row = this.sourceStatement.get({
20
- spaceId, episodeId,
21
- bodyChars: normalized.maxBodyBytes,
22
- bodyBytes: normalized.maxBodyBytes,
23
- metadataChars: MAX_EPISODE_METADATA_BYTES,
24
- metadataBytes: MAX_EPISODE_METADATA_BYTES,
25
- includeRestricted: Number(normalized.includeRestricted)
26
- });
27
- return previewEpisode(mapSourceRow(row), normalized);
28
- }
29
-
30
- corrections(spaceId, factIds, options = {}) {
31
- const normalized = normalizeCorrectionEvidencePreviewOptions(options);
32
- if (factIds.length === 0) return [];
33
- const statement = this.#correctionStatement(factIds.length);
34
- const parameters = {
35
- spaceId,
36
- bodyChars: normalized.maxBodyBytes,
37
- bodyBytes: normalized.maxBodyBytes,
38
- fieldChars: MAX_EPISODE_METADATA_FIELD_BYTES,
39
- fieldBytes: MAX_EPISODE_METADATA_FIELD_BYTES,
40
- correctionLimit: normalized.maxCorrectionsPerFact,
41
- includeRestricted: Number(normalized.includeRestricted)
42
- };
43
- factIds.forEach((factId, index) => { parameters[`factId${index}`] = factId; });
44
- const groups = [...new Set(factIds)].map((factId) => ({
45
- factId,
46
- episodes: [],
47
- truncated: false
48
- }));
49
- const byFactId = new Map(groups.map((group) => [group.factId, group]));
50
- for (const row of statement.all(parameters)) {
51
- const group = byFactId.get(row.fact_id);
52
- group.episodes.push(previewEpisode(
53
- mapCorrectionRow(row),
54
- normalized,
55
- { correction: true }
56
- ));
57
- group.truncated = row.correction_count > normalized.maxCorrectionsPerFact;
58
- }
59
- return groups;
60
- }
61
-
62
- #correctionStatement(factCount) {
63
- if (!this.correctionStatements.has(factCount)) {
64
- this.correctionStatements.set(factCount, this.db.prepare(correctionQuery(factCount)));
65
- }
66
- return this.correctionStatements.get(factCount);
67
- }
68
- }
69
-
70
- function sourceQuery() {
71
- return `
72
- SELECT id, space_id, source_kind,
73
- substr(body, 1, @bodyChars) AS body_preview,
74
- length(CAST(body AS BLOB)) > @bodyBytes AS body_truncated,
75
- source_uri,
76
- substr(metadata_json, 1, @metadataChars) AS metadata_preview,
77
- length(CAST(metadata_json AS BLOB)) > @metadataBytes AS metadata_truncated,
78
- created_at
79
- FROM episodes source
80
- WHERE id = @episodeId AND space_id = @spaceId
81
- AND (@includeRestricted = 1 OR NOT ${sqliteEpisodeRestrictedSql('source')})
82
- `;
83
- }
84
-
85
- function correctionQuery(factCount) {
86
- const factIds = Array.from({ length: factCount }, (_, index) => `@factId${index}`).join(', ');
87
- const fields = ['kind', 'factId', 'action'];
88
- const selected = fields.map((field) => `
89
- substr(CAST(json_extract(metadata_json, '$.${field}') AS TEXT), 1, @fieldChars)
90
- AS metadata_${field.toLowerCase()}`
91
- ).join(',');
92
- const clipped = fields.map((field) => `
93
- length(CAST(json_extract(metadata_json, '$.${field}') AS BLOB)) > @fieldBytes`
94
- ).join(' OR ');
95
- return `
96
- WITH ranked AS (
97
- SELECT correction.rowid AS correction_rowid,
98
- json_extract(metadata_json, '$.factId') AS fact_id,
99
- row_number() OVER (
100
- PARTITION BY json_extract(metadata_json, '$.factId')
101
- ORDER BY created_at DESC, correction.rowid DESC
102
- ) AS correction_rank,
103
- count(*) OVER (
104
- PARTITION BY json_extract(metadata_json, '$.factId')
105
- ) AS correction_count
106
- FROM episodes correction
107
- WHERE space_id = @spaceId
108
- AND json_extract(metadata_json, '$.kind') = 'lens_correction'
109
- AND json_extract(metadata_json, '$.factId') IN (${factIds})
110
- AND (@includeRestricted = 1 OR NOT ${sqliteEpisodeRestrictedSql('correction')})
111
- )
112
- SELECT correction.id, correction.space_id, correction.source_kind,
113
- substr(body, 1, @bodyChars) AS body_preview,
114
- length(CAST(body AS BLOB)) > @bodyBytes AS body_truncated,
115
- source_uri,${selected}, ranked.fact_id, ranked.correction_count,
116
- ((${clipped}) OR EXISTS (
117
- SELECT 1 FROM json_each(metadata_json)
118
- WHERE key NOT IN ('kind', 'factId', 'action')
119
- )) AS metadata_truncated,
120
- correction.created_at
121
- FROM ranked
122
- JOIN episodes correction ON correction.rowid = ranked.correction_rowid
123
- WHERE ranked.correction_rank <= @correctionLimit
124
- ORDER BY ranked.fact_id, ranked.correction_rank
125
- `;
126
- }
127
-
128
- function mapSourceRow(row) {
129
- if (!row) return null;
130
- return mapRow(row, row.metadata_truncated ? {} : JSON.parse(row.metadata_preview));
131
- }
132
-
133
- function mapCorrectionRow(row) {
134
- if (!row) return null;
135
- const metadata = {};
136
- for (const [key, column] of [
137
- ['kind', 'metadata_kind'], ['factId', 'metadata_factid'], ['action', 'metadata_action']
138
- ]) if (typeof row[column] === 'string') metadata[key] = row[column];
139
- return mapRow(row, metadata);
140
- }
141
-
142
- function mapRow(row, metadata) {
143
- return {
144
- id: row.id,
145
- spaceId: row.space_id,
146
- sourceKind: row.source_kind,
147
- body: row.body_preview,
148
- bodyTruncated: row.body_truncated === 1,
149
- sourceUri: row.source_uri,
150
- metadata,
151
- metadataTruncated: row.metadata_truncated === 1,
152
- createdAt: row.created_at
153
- };
154
- }
@@ -1,33 +0,0 @@
1
- import { isRestrictedEpisode } from '../../security/episode-visibility.js';
2
-
3
- const FUNCTION_NAME = 'episode_evidence_is_restricted';
4
-
5
- export function registerEpisodePrivacyFunction(db) {
6
- db.function(
7
- FUNCTION_NAME,
8
- { deterministic: true },
9
- (sourceKind, body, sourceUri, metadataJson) => Number(isRestrictedEpisode({
10
- sourceKind,
11
- body,
12
- sourceUri,
13
- metadata: parseMetadata(metadataJson)
14
- }))
15
- );
16
- }
17
-
18
- export function sqliteEpisodeRestrictedSql(alias) {
19
- return `${FUNCTION_NAME}(
20
- ${alias}.source_kind,
21
- ${alias}.body,
22
- ${alias}.source_uri,
23
- ${alias}.metadata_json
24
- ) = 1`;
25
- }
26
-
27
- function parseMetadata(value) {
28
- try {
29
- return JSON.parse(value);
30
- } catch {
31
- return { invalidMetadataRestricted: 'password=invalid-metadata' };
32
- }
33
- }
@@ -1,52 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
-
3
- import { nowIso } from '../../models.js';
4
- import { mapEpisode } from './mapper.js';
5
-
6
- export class EpisodesRepository {
7
- constructor(db) {
8
- this.insertStatement = db.prepare(`
9
- INSERT INTO episodes (
10
- id, space_id, source_kind, body, source_uri, metadata_json, created_at
11
- ) VALUES (
12
- @id, @spaceId, @sourceKind, @body, @sourceUri, @metadataJson, @createdAt
13
- )
14
- `);
15
- this.getStatement = db.prepare('SELECT * FROM episodes WHERE id = ?');
16
- this.listStatement = db.prepare('SELECT * FROM episodes ORDER BY rowid');
17
- this.deleteStatement = db.prepare('DELETE FROM episodes');
18
- }
19
-
20
- add(spaceId, sourceKind, body, sourceUri = null, metadata = {}) {
21
- const episode = {
22
- id: randomUUID(),
23
- spaceId,
24
- sourceKind,
25
- body,
26
- sourceUri,
27
- metadataJson: JSON.stringify(metadata),
28
- createdAt: nowIso()
29
- };
30
- this.insertStatement.run(episode);
31
- return this.get(episode.id);
32
- }
33
-
34
- get(id) {
35
- return mapEpisode(this.getStatement.get(id));
36
- }
37
-
38
- list() {
39
- return this.listStatement.all().map(mapEpisode);
40
- }
41
-
42
- insertSnapshot(episode) {
43
- this.insertStatement.run({
44
- ...episode,
45
- metadataJson: JSON.stringify(episode.metadata)
46
- });
47
- }
48
-
49
- deleteAll() {
50
- this.deleteStatement.run();
51
- }
52
- }
@@ -1,162 +0,0 @@
1
- import { isRestrictedFact } from '../../security/fact-visibility.js';
2
- import { searchTextSortKey } from '../fact-search-order.js';
3
- import { sqliteEpisodeRestrictedSql } from './episode-privacy-sql.js';
4
- import { mapFact } from './mapper.js';
5
-
6
- export class FactSearchRepository {
7
- constructor(db) {
8
- this.db = db;
9
- this.statements = new Map();
10
- this.omissionStatements = new Map();
11
- registerSearchFunctions(db);
12
- }
13
-
14
- search(spaceIds, query, {
15
- includeHistorical = false,
16
- scope = null,
17
- limit = null,
18
- excludeRestricted = false,
19
- safeSourceSpaceId = null,
20
- maxObjectBytes = null
21
- } = {}) {
22
- return this.searchPage(spaceIds, query, {
23
- includeHistorical,
24
- scope,
25
- limit,
26
- excludeRestricted,
27
- safeSourceSpaceId,
28
- maxObjectBytes
29
- }).facts;
30
- }
31
-
32
- searchPage(spaceIds, query, options = {}) {
33
- const {
34
- includeHistorical = false,
35
- scope = null,
36
- limit = null,
37
- excludeRestricted = false,
38
- safeSourceSpaceId = null,
39
- maxObjectBytes = null
40
- } = options;
41
- if (spaceIds.length === 0) return { facts: [], objectLimitTruncated: false };
42
- if (limit !== null && (!Number.isInteger(limit) || limit <= 0)) {
43
- throw new TypeError('Fact search limit must be a positive integer');
44
- }
45
-
46
- const shape = {
47
- spaceCount: spaceIds.length,
48
- includeHistorical,
49
- scoped: scope !== null,
50
- limited: limit !== null,
51
- excludeRestricted: excludeRestricted === true,
52
- safeSourceRequired: safeSourceSpaceId !== null,
53
- objectBytesBounded: maxObjectBytes !== null
54
- };
55
- const parameters = [...spaceIds];
56
- if (scope !== null) parameters.push(scope);
57
- if (safeSourceSpaceId !== null) parameters.push(safeSourceSpaceId);
58
- if (maxObjectBytes !== null) parameters.push(maxObjectBytes);
59
- parameters.push(query.toLowerCase());
60
- const factParameters = limit === null ? parameters : [...parameters, limit];
61
- const facts = this.#statement(shape).all(...factParameters).map(mapFact);
62
- const objectLimitTruncated = maxObjectBytes === null
63
- ? false
64
- : this.#omissionStatement(shape).pluck().get(...parameters) === 1;
65
- return { facts, objectLimitTruncated };
66
- }
67
-
68
- #statement(shape) {
69
- const key = Object.values(shape).join(':');
70
- if (!this.statements.has(key)) {
71
- this.statements.set(key, this.db.prepare(buildSearchQuery(shape)));
72
- }
73
- return this.statements.get(key);
74
- }
75
-
76
- #omissionStatement(shape) {
77
- const key = Object.values(shape).join(':');
78
- if (!this.omissionStatements.has(key)) {
79
- this.omissionStatements.set(key, this.db.prepare(buildOmissionQuery(shape)));
80
- }
81
- return this.omissionStatements.get(key);
82
- }
83
- }
84
-
85
- function buildOmissionQuery(shape) {
86
- return `SELECT EXISTS (
87
- SELECT 1 FROM facts
88
- WHERE ${buildWhereClause(shape, { oversized: true })}
89
- LIMIT 1
90
- )`;
91
- }
92
-
93
- function registerSearchFunctions(db) {
94
- db.function('fact_search_text_sort_key', { deterministic: true }, searchTextSortKey);
95
- db.function('fact_search_is_restricted', { deterministic: true },
96
- (subject, predicate, object, sensitivity) => Number(isRestrictedFact({
97
- subject, predicate, object, sensitivity
98
- })));
99
- }
100
-
101
- function buildSearchQuery({
102
- spaceCount,
103
- includeHistorical,
104
- scoped,
105
- limited,
106
- excludeRestricted,
107
- safeSourceRequired,
108
- objectBytesBounded
109
- }) {
110
- const shape = {
111
- spaceCount,
112
- includeHistorical,
113
- scoped,
114
- excludeRestricted,
115
- safeSourceRequired,
116
- objectBytesBounded
117
- };
118
- const orderClause = limited
119
- ? 'ORDER BY valid_at DESC, fact_search_text_sort_key(id) ASC LIMIT ?'
120
- : 'ORDER BY rowid';
121
- return `
122
- SELECT * FROM facts
123
- WHERE ${buildWhereClause(shape)}
124
- ${orderClause}
125
- `;
126
- }
127
-
128
- function buildWhereClause({
129
- spaceCount,
130
- includeHistorical,
131
- scoped,
132
- excludeRestricted,
133
- safeSourceRequired,
134
- objectBytesBounded
135
- }, { oversized = false } = {}) {
136
- const placeholders = Array.from({ length: spaceCount }, () => '?').join(', ');
137
- const currentClause = includeHistorical
138
- ? ''
139
- : "AND invalid_at IS NULL AND status NOT IN ('rejected', 'deprecated')";
140
- const scopeClause = scoped ? 'AND scope = ?' : '';
141
- const restrictedClause = excludeRestricted
142
- ? `AND fact_search_is_restricted(
143
- subject, predicate, object, sensitivity
144
- ) = 0`
145
- : '';
146
- const sourceClause = safeSourceRequired ? `AND EXISTS (
147
- SELECT 1 FROM episodes source
148
- WHERE source.id = source_episode_id
149
- AND source.space_id = ?
150
- AND NOT ${sqliteEpisodeRestrictedSql('source')}
151
- )` : '';
152
- const objectBytesClause = objectBytesBounded
153
- ? `AND length(CAST(object AS BLOB)) ${oversized ? '>' : '<='} ?`
154
- : '';
155
- return `space_id IN (${placeholders})
156
- ${currentClause}
157
- ${scopeClause}
158
- ${restrictedClause}
159
- ${sourceClause}
160
- ${objectBytesClause}
161
- AND instr(lower(subject || ' ' || predicate || ' ' || object), ?) > 0`;
162
- }