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,206 +0,0 @@
1
- import { isCurrentFact } from './models.js';
2
- import { listCandidates } from './candidates.js';
3
- import { getProjectRules } from './project-rules.js';
4
- import { getFactHistory } from './fact-history.js';
5
-
6
- export function buildContextPack(store, { personalSpaceId, spaceId, query = '' }) {
7
- const personalSpace = store.getSpace(personalSpaceId);
8
- const space = store.getSpace(spaceId);
9
- const scopedSpaceIds = [...new Set([personalSpaceId, spaceId])];
10
- const matches = store
11
- .searchFacts(scopedSpaceIds, query, { includeHistorical: false })
12
- .map((fact) => compactMatch(store, fact));
13
- const histories = buildHistories(store, matches);
14
- const rules = compactRules(getProjectRules(store, spaceId), store);
15
- const candidates = listCandidates(store, personalSpaceId)
16
- .filter((candidate) => !candidate.targetSpaceId || candidate.targetSpaceId === spaceId)
17
- .map(compactCandidate);
18
-
19
- return {
20
- query,
21
- personalSpace: compactSpace(personalSpace, personalSpaceId),
22
- space: compactSpace(space, spaceId),
23
- rules,
24
- matches,
25
- histories,
26
- candidateCount: candidates.length,
27
- candidates,
28
- answer: formatPackAnswer(matches)
29
- };
30
- }
31
-
32
- export function formatContextPack(pack) {
33
- const lines = [
34
- `Context ${pack.personalSpace.name} -> ${pack.space.name}`,
35
- `Query ${pack.query || '(empty)'}`,
36
- 'Rules',
37
- ...formatRules(pack.rules),
38
- 'Matches',
39
- ...formatMatches(pack.matches),
40
- ...formatHistories(pack.histories),
41
- `Candidates ${pack.candidateCount}`
42
- ];
43
-
44
- return lines.join('\n');
45
- }
46
-
47
- function compactRules(rules, store) {
48
- return {
49
- spaceId: rules.spaceId,
50
- spaceName: rules.spaceName,
51
- forbidden: rules.forbidden.map((fact) => compactFact(store, fact)),
52
- parameters: rules.parameters.map((fact) => compactFact(store, fact)),
53
- links: rules.links.map((fact) => compactFact(store, fact))
54
- };
55
- }
56
-
57
- function compactFact(store, fact) {
58
- return {
59
- id: fact.id,
60
- spaceId: fact.spaceId,
61
- spaceName: store.getSpace(fact.spaceId)?.name ?? fact.spaceId,
62
- subject: fact.subject,
63
- predicate: fact.predicate,
64
- object: fact.object,
65
- current: isCurrentFact(fact),
66
- validAt: fact.validAt,
67
- invalidAt: fact.invalidAt,
68
- source: compactSource(store, fact.sourceEpisodeId)
69
- };
70
- }
71
-
72
- function compactMatch(store, fact) {
73
- const compact = compactFact(store, fact);
74
- return {
75
- spaceId: compact.spaceId,
76
- spaceName: compact.spaceName,
77
- current: compact.current,
78
- fact: {
79
- id: compact.id,
80
- spaceId: compact.spaceId,
81
- subject: compact.subject,
82
- predicate: compact.predicate,
83
- object: compact.object,
84
- validAt: compact.validAt,
85
- invalidAt: compact.invalidAt
86
- },
87
- source: compact.source
88
- };
89
- }
90
-
91
- function buildHistories(store, matches) {
92
- const keys = new Set();
93
- const histories = [];
94
-
95
- for (const match of matches) {
96
- const key = `${match.fact.spaceId}:${match.fact.predicate}`;
97
- if (keys.has(key)) continue;
98
- keys.add(key);
99
-
100
- const history = getFactHistory(store, {
101
- spaceId: match.fact.spaceId,
102
- predicate: match.fact.predicate
103
- });
104
- if (history.facts.length <= 1) continue;
105
-
106
- histories.push({
107
- spaceId: history.spaceId,
108
- spaceName: history.spaceName,
109
- predicate: history.predicate,
110
- facts: history.facts.map((fact) => compactFact(store, fact))
111
- });
112
- }
113
-
114
- return histories;
115
- }
116
-
117
- function compactCandidate(candidate) {
118
- return {
119
- id: candidate.id,
120
- reason: candidate.reason,
121
- createdAt: candidate.createdAt,
122
- targetSpaceId: candidate.targetSpaceId,
123
- targetSpaceName: candidate.targetSpaceName,
124
- source: candidate.episode
125
- ? {
126
- id: candidate.episode.id,
127
- kind: candidate.episode.sourceKind,
128
- uri: candidate.episode.sourceUri,
129
- createdAt: candidate.episode.createdAt,
130
- preview: firstLine(candidate.episode.body)
131
- }
132
- : null
133
- };
134
- }
135
-
136
- function compactSource(store, episodeId) {
137
- const episode = store.getEpisode(episodeId);
138
- if (!episode) return null;
139
-
140
- return {
141
- id: episode.id,
142
- kind: episode.sourceKind,
143
- uri: episode.sourceUri,
144
- createdAt: episode.createdAt,
145
- preview: firstLine(episode.body)
146
- };
147
- }
148
-
149
- function compactSpace(space, fallbackId) {
150
- return {
151
- id: space?.id ?? fallbackId,
152
- name: space?.name ?? fallbackId,
153
- kind: space?.kind ?? null
154
- };
155
- }
156
-
157
- function firstLine(value) {
158
- return String(value).split(/\r?\n/)[0].slice(0, 160);
159
- }
160
-
161
- function formatPackAnswer(matches) {
162
- if (matches.length === 0) {
163
- return 'No matching current facts found.';
164
- }
165
-
166
- return matches
167
- .map((match) => `${match.fact.subject} ${match.fact.predicate} ${match.fact.object}`)
168
- .join('\n');
169
- }
170
-
171
- function formatRules(rules) {
172
- const lines = [
173
- ...rules.forbidden.map((fact) => `forbids ${fact.object}`),
174
- ...rules.parameters.map((fact) => `${humanPredicate(fact.predicate)} ${fact.object}`),
175
- ...rules.links.map((fact) => `url ${fact.object}`)
176
- ];
177
-
178
- return lines.length ? lines : ['(none)'];
179
- }
180
-
181
- function formatMatches(matches) {
182
- return matches.length
183
- ? matches.map(
184
- (match) =>
185
- `${match.spaceName} ${humanPredicate(match.fact.predicate)} ${match.fact.object}`
186
- )
187
- : ['(none)'];
188
- }
189
-
190
- function formatHistories(histories) {
191
- if (!histories?.length) return [];
192
-
193
- return [
194
- 'History',
195
- ...histories.flatMap((history) =>
196
- history.facts.map(
197
- (fact) =>
198
- `${fact.current ? 'current' : 'historical'} ${humanPredicate(history.predicate)} ${fact.object}`
199
- )
200
- )
201
- ];
202
- }
203
-
204
- function humanPredicate(predicate) {
205
- return predicate.startsWith('has_') ? predicate.slice(4) : predicate;
206
- }
package/src/extractor.js DELETED
@@ -1,77 +0,0 @@
1
- const URL_PATTERN = /https?:\/\/[^\s"',。))]+/g;
2
-
3
- export function extractFactSpecs(subject, body) {
4
- const lines = body
5
- .split(/\r?\n/)
6
- .map((line) => line.trim())
7
- .filter(Boolean);
8
- const specs = [];
9
-
10
- for (const line of lines) {
11
- const replacement = parseReplacement(subject, line);
12
- if (replacement) {
13
- specs.push(replacement);
14
- continue;
15
- }
16
-
17
- const forbidden = parseForbidden(subject, line);
18
- if (forbidden) {
19
- specs.push(forbidden);
20
- continue;
21
- }
22
-
23
- const keyValue = parseKeyValue(subject, line);
24
- if (keyValue) {
25
- specs.push(keyValue);
26
- continue;
27
- }
28
-
29
- for (const url of line.match(URL_PATTERN) ?? []) {
30
- specs.push({ kind: 'fact', subject, predicate: 'has_url', object: url });
31
- }
32
- }
33
-
34
- return specs;
35
- }
36
-
37
- function parseReplacement(subject, line) {
38
- const match = line.match(/^替代[::]\s*(.+?)\s*=>\s*(.+)$/);
39
- if (!match) return null;
40
-
41
- return {
42
- kind: 'replacement',
43
- subject,
44
- oldValue: match[1].trim(),
45
- newValue: match[2].trim()
46
- };
47
- }
48
-
49
- function parseForbidden(subject, line) {
50
- const match =
51
- line.match(/^禁止[::]\s*(.+)$/) ??
52
- line.match(/^(?:这个项目|本项目|项目)?\s*(?:禁止|不要|不允许)\s*(?:使用|采用|引入|用)?\s*(.+)$/);
53
- if (!match) return null;
54
-
55
- return {
56
- kind: 'fact',
57
- subject,
58
- predicate: 'forbids',
59
- object: cleanForbiddenObject(match[1])
60
- };
61
- }
62
-
63
- function cleanForbiddenObject(value) {
64
- return value.trim().replace(/[。.!!,,;;]+$/g, '').trim();
65
- }
66
-
67
- function parseKeyValue(subject, line) {
68
- const match = line.match(/^([\p{L}\p{N}_.-]+)\s*[::]\s*(.+)$/u);
69
- if (!match) return null;
70
-
71
- return {
72
- kind: 'fact',
73
- subject,
74
- predicate: `has_${match[1].trim()}`,
75
- object: match[2].trim()
76
- };
77
- }
@@ -1,39 +0,0 @@
1
- import { isCurrentFact } from './models.js';
2
-
3
- export function getFactHistory(store, { spaceId, predicate }) {
4
- const normalizedPredicate = normalizePredicate(predicate);
5
- const facts = store.listFacts({ includeHistorical: true })
6
- .filter((fact) => fact.spaceId === spaceId && fact.predicate === normalizedPredicate)
7
- .sort((left, right) => left.validAt.localeCompare(right.validAt))
8
- .map((fact) => enrichFact(store, fact));
9
-
10
- return {
11
- spaceId,
12
- spaceName: store.getSpace(spaceId)?.name ?? spaceId,
13
- predicate: normalizedPredicate,
14
- facts
15
- };
16
- }
17
-
18
- export function normalizePredicate(value) {
19
- if (value.startsWith('has_') || value === 'forbids') return value;
20
- return `has_${value}`;
21
- }
22
-
23
- function enrichFact(store, fact) {
24
- const episode = store.getEpisode(fact.sourceEpisodeId);
25
-
26
- return {
27
- ...fact,
28
- current: isCurrentFact(fact),
29
- source: episode
30
- ? {
31
- id: episode.id,
32
- kind: episode.sourceKind,
33
- uri: episode.sourceUri,
34
- body: episode.body,
35
- createdAt: episode.createdAt
36
- }
37
- : null
38
- };
39
- }
@@ -1,89 +0,0 @@
1
- import { FactScope, FactStatus } from '../models.js';
2
-
3
- export class FactService {
4
- constructor(store) {
5
- this.store = store;
6
- }
7
-
8
- writeSpecs({ spaceId, subject, episodeId, specs, scope = FactScope.PERSONAL }) {
9
- const written = [];
10
- for (const spec of specs) {
11
- if (spec.kind === 'replacement') {
12
- written.push(...this.#replaceFact({ spaceId, subject, episodeId, spec, scope }));
13
- continue;
14
- }
15
-
16
- if (this.#hasCurrentFact(spaceId, subject, spec.predicate, spec.object)) continue;
17
-
18
- const supersededFacts = this.#supersededCurrentFacts(
19
- spaceId,
20
- subject,
21
- spec.predicate,
22
- spec.object
23
- );
24
- const newFact = this.store.addFact({
25
- spaceId,
26
- subject,
27
- predicate: spec.predicate,
28
- object: spec.object,
29
- sourceEpisodeId: episodeId,
30
- status: FactStatus.CONFIRMED,
31
- scope
32
- });
33
- written.push(newFact);
34
-
35
- for (const oldFact of supersededFacts) {
36
- this.store.invalidateFact(oldFact.id, newFact.id);
37
- }
38
- }
39
- return written;
40
- }
41
-
42
- #hasCurrentFact(spaceId, subject, predicate, object) {
43
- return this.store.currentFacts(spaceId).some(
44
- (fact) =>
45
- fact.subject === subject &&
46
- fact.predicate === predicate &&
47
- fact.object === object
48
- );
49
- }
50
-
51
- #supersededCurrentFacts(spaceId, subject, predicate, object) {
52
- if (!isUniqueParameterPredicate(predicate)) return [];
53
-
54
- return this.store.currentFacts(spaceId).filter(
55
- (fact) =>
56
- fact.subject === subject &&
57
- fact.predicate === predicate &&
58
- fact.object !== object
59
- );
60
- }
61
-
62
- #replaceFact({ spaceId, subject, episodeId, spec, scope }) {
63
- const oldFacts = this.store.currentFacts(spaceId).filter(
64
- (fact) => fact.subject === subject && fact.object === spec.oldValue
65
- );
66
- const predicates = oldFacts.length
67
- ? [...new Set(oldFacts.map((fact) => fact.predicate))]
68
- : ['has_replacement'];
69
- const newFacts = predicates.map((predicate) => this.store.addFact({
70
- spaceId,
71
- subject,
72
- predicate,
73
- object: spec.newValue,
74
- sourceEpisodeId: episodeId,
75
- status: FactStatus.CONFIRMED,
76
- scope
77
- }));
78
-
79
- for (const oldFact of oldFacts) {
80
- const replacement = newFacts.find((fact) => fact.predicate === oldFact.predicate) ?? newFacts[0];
81
- this.store.invalidateFact(oldFact.id, replacement.id);
82
- }
83
- return newFacts;
84
- }
85
- }
86
-
87
- function isUniqueParameterPredicate(predicate) {
88
- return predicate.startsWith('has_') && predicate !== 'has_url';
89
- }
@@ -1,19 +0,0 @@
1
- export function selectBoundedJsonArray(items, budget) {
2
- const selected = [];
3
- let usedBytes = Buffer.byteLength('[]', 'utf8');
4
- let truncated = false;
5
-
6
- for (const item of items) {
7
- const serialized = JSON.stringify(item);
8
- const itemBytes = Buffer.byteLength(serialized, 'utf8');
9
- const nextBytes = usedBytes + itemBytes + (selected.length ? 1 : 0);
10
- if (nextBytes > budget) {
11
- truncated = true;
12
- continue;
13
- }
14
- selected.push(item);
15
- usedBytes = nextBytes;
16
- }
17
-
18
- return { items: selected, usedBytes, truncated };
19
- }
@@ -1,50 +0,0 @@
1
- import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
- import { selectBoundedJsonArray } from './bounded-json-array.js';
3
- import { sendJson } from './response.js';
4
-
5
- const DEFAULT_BUDGET = 1200;
6
- const MAX_BUDGET = 16384;
7
-
8
- export function handleLensRequest({ request, response, url, app }) {
9
- if (url.pathname !== '/api/lens' || request.method !== 'GET') return false;
10
-
11
- const personalSpaceId = app.requireSpaceId({
12
- id: url.searchParams.get('personalSpaceId'),
13
- name: url.searchParams.get('personalSpaceName'),
14
- label: 'Personal space'
15
- });
16
- const budget = parseBudget(url.searchParams.get('budget'));
17
- const view = app.lens.getUserLensView({
18
- personalSpaceId,
19
- task: '',
20
- budget,
21
- includeObserved: true,
22
- includeSuggested: false,
23
- includeRestricted: false
24
- });
25
- const bounded = selectBoundedJsonArray(view.entries, budget);
26
-
27
- sendJson(response, 200, {
28
- personalSpaceId,
29
- budget,
30
- usedBytes: bounded.usedBytes,
31
- truncated: view.truncated || bounded.truncated,
32
- facts: bounded.items
33
- });
34
- return true;
35
- }
36
-
37
- function parseBudget(value) {
38
- if (value === null) return DEFAULT_BUDGET;
39
- if (!/^[1-9]\d*$/.test(value)) throw invalidBudget();
40
- const budget = Number(value);
41
- if (!Number.isSafeInteger(budget) || budget < 2 || budget > MAX_BUDGET) throw invalidBudget();
42
- return budget;
43
- }
44
-
45
- function invalidBudget() {
46
- return new ApplicationError(
47
- ApplicationErrorCode.VALIDATION,
48
- `Budget must be an integer between 2 and ${MAX_BUDGET}`
49
- );
50
- }
package/src/ingestion.js DELETED
@@ -1,161 +0,0 @@
1
- import { classifyEpisode } from './classifier.js';
2
- import { extractFactSpecs } from './extractor.js';
3
- import { FactService } from './facts/fact-service.js';
4
- import { LensService } from './lens/lens-service.js';
5
- import { ApplicationError, ApplicationErrorCode } from './app/application-error.js';
6
- import { CandidateStatus, FactScope, PublishRoute, SpaceKind } from './models.js';
7
- import { detectSensitiveContent } from './security/sensitive-content.js';
8
- import { assertSafeSourceMetadata } from './security/source-metadata-policy.js';
9
- import { PublicationService } from './publication/publication-service.js';
10
-
11
- export class IngestionService {
12
- constructor(store, {
13
- facts = new FactService(store),
14
- lens = new LensService(store),
15
- publication = new PublicationService(store)
16
- } = {}) {
17
- this.store = store;
18
- this.facts = facts;
19
- this.lens = lens;
20
- this.publication = publication;
21
- }
22
-
23
- remember(input) {
24
- this.#assertSafeInput(input);
25
- return this.store.transaction(
26
- () => this.rememberInCurrentTransaction(input),
27
- { mode: 'immediate' }
28
- );
29
- }
30
-
31
- rememberInCurrentTransaction({
32
- personalSpaceId,
33
- sourceKind,
34
- body,
35
- targetSpaceId = null,
36
- sourceUri = null
37
- }) {
38
- this.#assertSafeInput({ body, sourceKind, sourceUri });
39
- const route = classifyEpisode({ sourceKind, body });
40
-
41
- if (route === PublishRoute.PERSONAL) {
42
- const result = this.lens.rememberUserFactsFromSourceInCurrentTransaction({
43
- personalSpaceId,
44
- subject: this.#spaceName(personalSpaceId),
45
- specs: extractFactSpecs(this.#spaceName(personalSpaceId), body),
46
- sourceText: body,
47
- sourceKind,
48
- sourceUri
49
- });
50
- return { route, ...result };
51
- }
52
-
53
- const personalEpisode = this.store.addEpisode(personalSpaceId, sourceKind, body, sourceUri);
54
-
55
- if (route === PublishRoute.PUBLIC && targetSpaceId) {
56
- const targetEpisode = this.store.addEpisode(targetSpaceId, sourceKind, body, sourceUri);
57
- const facts = this.facts.writeSpecs({
58
- spaceId: targetSpaceId,
59
- subject: this.#spaceName(targetSpaceId),
60
- episodeId: targetEpisode.id,
61
- specs: extractFactSpecs(this.#spaceName(targetSpaceId), body),
62
- scope: FactScope.PUBLIC
63
- });
64
- const publication = facts.length === 0
65
- ? null
66
- : this.publication.prepareInCurrentTransaction({
67
- spaceId: targetSpaceId,
68
- episode: targetEpisode,
69
- facts
70
- });
71
- return { route, episode: targetEpisode, publication };
72
- }
73
-
74
- const candidate = this.store.addCandidate({
75
- personalSpaceId,
76
- targetSpaceId,
77
- episodeId: personalEpisode.id,
78
- reason: 'not safe enough to publish automatically',
79
- status: CandidateStatus.PENDING
80
- });
81
- return { route: PublishRoute.CANDIDATE, episode: personalEpisode, candidate };
82
- }
83
-
84
- publishConfirmed(input) {
85
- this.#assertSafeInput(input);
86
- return this.store.transaction(
87
- () => this.publishConfirmedInCurrentTransaction(input),
88
- { mode: 'immediate' }
89
- );
90
- }
91
-
92
- publishConfirmedInCurrentTransaction({ targetSpaceId, sourceKind, body, sourceUri = null }) {
93
- this.#assertSafeInput({ body, sourceKind, sourceUri });
94
- const targetEpisode = this.store.addEpisode(targetSpaceId, sourceKind, body, sourceUri);
95
- const result = this.confirmEpisodeInCurrentTransaction({
96
- spaceId: targetSpaceId,
97
- episode: targetEpisode
98
- });
99
- return result;
100
- }
101
-
102
- confirmEpisode(input) {
103
- this.#assertSafeEpisode(input.episode);
104
- return this.store.transaction(
105
- () => this.confirmEpisodeInCurrentTransaction(input),
106
- { mode: 'immediate' }
107
- );
108
- }
109
-
110
- confirmEpisodeInCurrentTransaction({ spaceId, episode }) {
111
- this.#assertSafeEpisode(episode);
112
- const subject = this.#spaceName(spaceId);
113
- const specs = extractFactSpecs(subject, episode.body);
114
- if (this.store.getSpace(spaceId)?.kind === SpaceKind.PERSONAL) {
115
- return this.lens.writeUserFactsForEpisodeInCurrentTransaction({
116
- personalSpaceId: spaceId,
117
- subject,
118
- specs,
119
- episode
120
- });
121
- }
122
- const facts = this.facts.writeSpecs({
123
- spaceId,
124
- subject,
125
- episodeId: episode.id,
126
- specs,
127
- scope: FactScope.PUBLIC
128
- });
129
- const publication = facts.length === 0
130
- ? null
131
- : this.publication.prepareInCurrentTransaction({ spaceId, episode, facts });
132
- return { episode, publication };
133
- }
134
-
135
- #spaceName(spaceId) {
136
- return this.store.getSpace(spaceId)?.name ?? spaceId;
137
- }
138
-
139
- #assertSafeInput({ body, sourceKind, sourceUri }) {
140
- this.#assertSafeBody(body);
141
- assertSafeSourceMetadata(
142
- sourceKind,
143
- sourceUri,
144
- 'Sensitive source metadata is not allowed in ingestion'
145
- );
146
- }
147
-
148
- #assertSafeEpisode(episode) {
149
- this.#assertSafeInput(episode);
150
- }
151
-
152
- #assertSafeBody(body) {
153
- if (detectSensitiveContent(body).restricted) {
154
- throw new ApplicationError(
155
- ApplicationErrorCode.VALIDATION,
156
- 'Sensitive content is not allowed in ingestion'
157
- );
158
- }
159
- }
160
-
161
- }