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.
- package/README.en.md +2 -333
- package/README.md +370 -202
- package/README.zh-CN.md +448 -0
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
- package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
- package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
- package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
- package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
- package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
- package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
- package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
- package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
- package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
- package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
- package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/console-CbDsKrsO.js +1 -0
- package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
- package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
- package/dist/web/assets/i18n-BQcns1PH.js +5 -0
- package/dist/web/assets/identity-B8K4VYg6.js +1 -0
- package/dist/web/assets/index-CB8Xmsc2.js +2 -0
- package/dist/web/assets/index-CZeuPh-U.css +1 -0
- package/dist/web/assets/model-eF52GgPF.js +1 -0
- package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
- package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
- package/dist/web/index.html +9 -6
- package/docs/external-knowledge-architecture.md +279 -0
- package/examples/external-knowledge/markdown-folder.mjs +235 -0
- package/graph-provider/fuli_graph/app.py +57 -1
- package/graph-provider/fuli_graph/graph_models.py +3 -0
- package/graph-provider/fuli_graph/graph_query.py +62 -16
- package/graph-provider/fuli_graph/knowledge_review.py +612 -0
- package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
- package/graph-provider/fuli_graph/knowledge_search.py +51 -4
- package/graph-provider/fuli_graph/models.py +19 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +5 -407
- package/package.json +7 -5
- package/skills/capturing-session-knowledge/SKILL.md +24 -4
- package/skills/flreview/SKILL.md +73 -0
- package/skills/flreview/agents/openai.yaml +6 -0
- package/src/agent-tools/graph-definitions.js +79 -4
- package/src/agent-tools/graph-handlers.js +12 -0
- package/src/cli/command-registry.js +3 -3
- package/src/cli/local-runtime-command.js +51 -28
- package/src/cli/local-runtime-options.js +14 -8
- package/src/cli/setup-agent-selection.js +12 -12
- package/src/cli/setup-command.js +41 -18
- package/src/cli/shell-argument.js +10 -0
- package/src/cli/uninstall-command.js +17 -15
- package/src/cli/update-command.js +38 -30
- package/src/external-knowledge/catalog.js +61 -0
- package/src/external-knowledge/conflict-policy.js +58 -0
- package/src/external-knowledge/connected-search.js +166 -0
- package/src/external-knowledge/connectors/custom.js +133 -0
- package/src/external-knowledge/connectors/feishu.js +461 -0
- package/src/external-knowledge/connectors/mcp.js +438 -0
- package/src/external-knowledge/connectors/notion.js +307 -0
- package/src/external-knowledge/connectors/retrieval-api.js +222 -0
- package/src/external-knowledge/document-mapping.js +151 -0
- package/src/external-knowledge/graph-projection.js +17 -0
- package/src/external-knowledge/index.js +8 -0
- package/src/external-knowledge/registry.js +91 -0
- package/src/external-knowledge/runtime.js +68 -0
- package/src/external-knowledge/safe-diagnostic.js +14 -0
- package/src/external-knowledge/service.js +811 -0
- package/src/graphiti/agent-knowledge-workflows.js +7 -2
- package/src/graphiti/federated-application.js +66 -41
- package/src/graphiti/knowledge-item-role.js +21 -0
- package/src/graphiti/knowledge-review.js +43 -0
- package/src/graphiti/project-profile-mapping.js +32 -0
- package/src/graphiti/provider-client.js +22 -1
- package/src/graphiti/source-marker.js +54 -3
- package/src/graphiti/task-knowledge-recall.js +250 -0
- package/src/http/api-router.js +23 -108
- package/src/http/external-knowledge-api-router.js +104 -0
- package/src/http/graph-api-router.js +12 -1
- package/src/http/request-policy.js +1 -1
- package/src/local-runtime/lifecycle.js +26 -15
- package/src/mcp/create-mcp-server.js +1 -0
- package/src/mcp/instructions.js +7 -7
- package/src/mcp/tool-annotations.js +6 -0
- package/src/mcp/tool-result.js +1 -0
- package/src/server/application-lifecycle.js +2 -11
- package/src/server/blocked-ports.js +11 -0
- package/src/server.js +36 -17
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +2 -1
- package/src/setup/claude-code-config.js +2 -1
- package/src/setup/codex-bootstrap.js +12 -7
- package/src/setup/container-runtime.js +25 -21
- package/src/setup/graph-runtime.js +148 -56
- package/src/setup/node-runtime.js +3 -3
- package/src/setup/options.js +2 -4
- package/src/setup/paths.js +18 -3
- package/src/setup/review-skill-adapters.js +49 -0
- package/src/setup/setup.js +26 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +317 -0
- package/src/system/runtime-settings.js +131 -0
- package/src/system/system-service.js +119 -0
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
- package/dist/web/assets/console-B_Ena2DS.js +0 -5
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-CjBwYSMX.js +0 -1
- package/dist/web/assets/index-CpLDx3of.css +0 -1
- package/dist/web/assets/index-DAzeFJSc.js +0 -2
- package/dist/web/assets/model-C-5RVIJ2.js +0 -1
- package/dist/web/assets/paths-BkULqmGI.js +0 -1
- package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
- package/src/agent/candidate-read-service.js +0 -53
- package/src/agent/context-pack-service.js +0 -83
- package/src/agent/public-read-policy.js +0 -39
- package/src/agent/public-read-service.js +0 -69
- package/src/agent/read-projection.js +0 -107
- package/src/agent/search-service.js +0 -61
- package/src/agent/space-boundary.js +0 -33
- package/src/app/bootstrap.js +0 -8
- package/src/app/create-application.js +0 -143
- package/src/app/state-service.js +0 -38
- package/src/candidates.js +0 -78
- package/src/classifier.js +0 -63
- package/src/cli/command-arguments.js +0 -14
- package/src/context-pack.js +0 -206
- package/src/extractor.js +0 -77
- package/src/fact-history.js +0 -39
- package/src/facts/fact-service.js +0 -89
- package/src/http/bounded-json-array.js +0 -19
- package/src/http/lens-route.js +0 -50
- package/src/ingestion.js +0 -161
- package/src/lens/lens-correction-service.js +0 -81
- package/src/lens/lens-fact-writer.js +0 -85
- package/src/lens/lens-input.js +0 -62
- package/src/lens/lens-query-projection.js +0 -87
- package/src/lens/lens-query-ranking.js +0 -62
- package/src/lens/lens-query.js +0 -143
- package/src/lens/lens-resource-projection.js +0 -93
- package/src/lens/lens-resource-selection.js +0 -45
- package/src/lens/lens-resource-service.js +0 -103
- package/src/lens/lens-search-query.js +0 -122
- package/src/lens/lens-service.js +0 -183
- package/src/models.js +0 -44
- package/src/observer.js +0 -83
- package/src/project-rules.js +0 -33
- package/src/publication/outbox-service.js +0 -46
- package/src/publication/publication-envelope-schema.js +0 -75
- package/src/publication/publication-envelope.js +0 -37
- package/src/publication/publication-policy.js +0 -82
- package/src/publication/publication-service.js +0 -48
- package/src/router.js +0 -58
- package/src/runtime-options.js +0 -138
- package/src/security/episode-visibility.js +0 -14
- package/src/security/fact-visibility.js +0 -15
- package/src/security/source-metadata-policy.js +0 -8
- package/src/storage/episode-evidence-preview.js +0 -100
- package/src/storage/fact-search-order.js +0 -18
- package/src/storage/file/episode-evidence.js +0 -37
- package/src/storage/file/episode-index.js +0 -59
- package/src/storage/file/fact-search.js +0 -60
- package/src/storage/file-store.js +0 -437
- package/src/storage/migrate.js +0 -128
- package/src/storage/migrations/001-initial.sql +0 -83
- package/src/storage/migrations/002-storage-integrity.sql +0 -113
- package/src/storage/record-validation.js +0 -46
- package/src/storage/snapshot-defaults.js +0 -54
- package/src/storage/sqlite/candidates-repository.js +0 -84
- package/src/storage/sqlite/episode-evidence-repository.js +0 -154
- package/src/storage/sqlite/episode-privacy-sql.js +0 -33
- package/src/storage/sqlite/episodes-repository.js +0 -52
- package/src/storage/sqlite/fact-search-repository.js +0 -162
- package/src/storage/sqlite/facts-repository.js +0 -146
- package/src/storage/sqlite/imports-repository.js +0 -39
- package/src/storage/sqlite/mapper.js +0 -84
- package/src/storage/sqlite/open-database.js +0 -15
- package/src/storage/sqlite/outbox-repository.js +0 -103
- package/src/storage/sqlite/snapshot-repository.js +0 -69
- package/src/storage/sqlite/spaces-repository.js +0 -52
- package/src/storage/sqlite/subscriptions-repository.js +0 -54
- package/src/storage/sqlite-store.js +0 -159
- package/src/storage/stable-json.js +0 -24
- package/src/storage/store-port.js +0 -20
- package/src/store.js +0 -2
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { CandidateStatus, FactStatus, isCurrentFact, nowIso } from '../models.js';
|
|
4
|
-
import { FileEpisodeIndex } from './file/episode-index.js';
|
|
5
|
-
import {
|
|
6
|
-
fileCorrectionEpisodeEvidencePreviews,
|
|
7
|
-
fileEpisodeEvidencePreview
|
|
8
|
-
} from './file/episode-evidence.js';
|
|
9
|
-
import { searchFileFacts, searchFileFactsPage } from './file/fact-search.js';
|
|
10
|
-
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
11
|
-
import {
|
|
12
|
-
validateCandidateRecord,
|
|
13
|
-
validateFactRecord,
|
|
14
|
-
validateOutboxRecord
|
|
15
|
-
} from './record-validation.js';
|
|
16
|
-
import { normalizeSnapshot } from './snapshot-defaults.js';
|
|
17
|
-
|
|
18
|
-
export class FileStore {
|
|
19
|
-
#committedData;
|
|
20
|
-
#episodeIndex;
|
|
21
|
-
|
|
22
|
-
constructor(filePath, { jsonFileIo } = {}) {
|
|
23
|
-
this.filePath = filePath;
|
|
24
|
-
this.memoryOnly = filePath === ':memory:';
|
|
25
|
-
this.jsonFileIo = jsonFileIo;
|
|
26
|
-
this.transactionActive = false;
|
|
27
|
-
this.transactionDirty = false;
|
|
28
|
-
this.data = this.memoryOnly ? normalizeSnapshot() : this.#load();
|
|
29
|
-
this.#committedData = cloneData(this.data);
|
|
30
|
-
this.#episodeIndex = new FileEpisodeIndex(this.data.episodes);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
transaction(fn, _options = {}) {
|
|
34
|
-
if (this.transactionActive) {
|
|
35
|
-
throw new TypeError('Nested FileStore transactions are not supported');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (fn.constructor?.name === 'AsyncFunction') {
|
|
39
|
-
throw new TypeError('Transaction callback must be synchronous');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const snapshot = cloneData(this.data);
|
|
43
|
-
this.transactionActive = true;
|
|
44
|
-
this.transactionDirty = false;
|
|
45
|
-
try {
|
|
46
|
-
const result = fn(this);
|
|
47
|
-
if (isThenable(result)) {
|
|
48
|
-
if (typeof result.catch === 'function') result.catch(() => {});
|
|
49
|
-
throw new TypeError('Transaction callback must be synchronous');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const dirty = this.transactionDirty;
|
|
53
|
-
this.transactionActive = false;
|
|
54
|
-
this.transactionDirty = false;
|
|
55
|
-
if (dirty) this.#commit();
|
|
56
|
-
return result;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
this.data = snapshot;
|
|
59
|
-
this.#episodeIndex.rebuild(this.data.episodes);
|
|
60
|
-
this.transactionActive = false;
|
|
61
|
-
this.transactionDirty = false;
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
createSpace(name, kind, description = null) {
|
|
67
|
-
const existing = this.findSpaceByName(name);
|
|
68
|
-
if (existing) return existing;
|
|
69
|
-
|
|
70
|
-
const space = {
|
|
71
|
-
id: randomUUID(),
|
|
72
|
-
name,
|
|
73
|
-
kind,
|
|
74
|
-
description,
|
|
75
|
-
createdAt: nowIso()
|
|
76
|
-
};
|
|
77
|
-
this.data.spaces.push(space);
|
|
78
|
-
this.#save();
|
|
79
|
-
return cloneData(space);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
listSpaces() {
|
|
83
|
-
return cloneData(this.data.spaces);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
findSpaceByName(name) {
|
|
87
|
-
return cloneData(this.data.spaces.find((space) => space.name === name) ?? null);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
getSpace(id) {
|
|
91
|
-
return cloneData(this.data.spaces.find((space) => space.id === id) ?? null);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
subscribe(personalSpaceId, spaceId, mode = 'latest') {
|
|
95
|
-
const existing = this.data.subscriptions.find(
|
|
96
|
-
(subscription) =>
|
|
97
|
-
subscription.personalSpaceId === personalSpaceId && subscription.spaceId === spaceId
|
|
98
|
-
);
|
|
99
|
-
if (existing) return cloneData(existing);
|
|
100
|
-
|
|
101
|
-
const subscription = { personalSpaceId, spaceId, mode, createdAt: nowIso() };
|
|
102
|
-
this.data.subscriptions.push(subscription);
|
|
103
|
-
this.#save();
|
|
104
|
-
return cloneData(subscription);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
listSubscriptions() {
|
|
108
|
-
return cloneData(this.data.subscriptions);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
subscriptionsFor(personalSpaceId) {
|
|
112
|
-
return cloneData(this.data.subscriptions.filter(
|
|
113
|
-
(subscription) => subscription.personalSpaceId === personalSpaceId
|
|
114
|
-
));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
addEpisode(spaceId, sourceKind, body, sourceUri = null, metadata = {}) {
|
|
118
|
-
const episode = {
|
|
119
|
-
id: randomUUID(),
|
|
120
|
-
spaceId,
|
|
121
|
-
sourceKind,
|
|
122
|
-
body,
|
|
123
|
-
sourceUri,
|
|
124
|
-
metadata: cloneData(metadata),
|
|
125
|
-
createdAt: nowIso()
|
|
126
|
-
};
|
|
127
|
-
this.data.episodes.push(episode);
|
|
128
|
-
this.#episodeIndex.add(episode);
|
|
129
|
-
this.#save();
|
|
130
|
-
return cloneData(episode);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
getEpisode(id) {
|
|
134
|
-
return cloneData(this.#episodeIndex.get(id));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
listEpisodes() {
|
|
138
|
-
return cloneData(this.data.episodes);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
episodeEvidencePreview(spaceId, episodeId, options = {}) {
|
|
142
|
-
return cloneData(fileEpisodeEvidencePreview(
|
|
143
|
-
this.#episodeIndex,
|
|
144
|
-
spaceId,
|
|
145
|
-
episodeId,
|
|
146
|
-
options
|
|
147
|
-
));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
correctionEpisodeEvidencePreviews(spaceId, factIds, options = {}) {
|
|
151
|
-
return cloneData(fileCorrectionEpisodeEvidencePreviews(
|
|
152
|
-
this.#episodeIndex,
|
|
153
|
-
spaceId,
|
|
154
|
-
factIds,
|
|
155
|
-
options
|
|
156
|
-
));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
addFact(fact) {
|
|
160
|
-
if (fact.id && this.data.facts.some((item) => item.id === fact.id)) {
|
|
161
|
-
throw new Error(`Duplicate fact id: ${fact.id}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const stored = {
|
|
165
|
-
id: fact.id || randomUUID(),
|
|
166
|
-
spaceId: fact.spaceId,
|
|
167
|
-
subject: fact.subject,
|
|
168
|
-
predicate: fact.predicate,
|
|
169
|
-
object: fact.object,
|
|
170
|
-
sourceEpisodeId: fact.sourceEpisodeId,
|
|
171
|
-
status: fact.status ?? FactStatus.CONFIRMED,
|
|
172
|
-
confidence: fact.confidence ?? 1,
|
|
173
|
-
sensitivity: fact.sensitivity ?? 'normal',
|
|
174
|
-
scope: fact.scope ?? 'personal',
|
|
175
|
-
validAt: fact.validAt ?? nowIso(),
|
|
176
|
-
invalidAt: fact.invalidAt ?? null,
|
|
177
|
-
replacedByFactId: fact.replacedByFactId ?? null
|
|
178
|
-
};
|
|
179
|
-
validateFactRecord(stored);
|
|
180
|
-
this.data.facts.push(stored);
|
|
181
|
-
this.#save();
|
|
182
|
-
return cloneData(stored);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
getFact(id) {
|
|
186
|
-
return cloneData(this.data.facts.find((fact) => fact.id === id) ?? null);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
updateFact(id, updates) {
|
|
190
|
-
const fact = this.data.facts.find((item) => item.id === id);
|
|
191
|
-
if (!fact) {
|
|
192
|
-
throw new Error(`Fact not found: ${id}`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const updated = { ...fact, ...updates, id };
|
|
196
|
-
validateFactRecord(updated);
|
|
197
|
-
Object.assign(fact, updated);
|
|
198
|
-
this.#save();
|
|
199
|
-
return cloneData(fact);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
invalidateFact(factId, replacedByFactId = null) {
|
|
203
|
-
const fact = this.data.facts.find((item) => item.id === factId);
|
|
204
|
-
if (!fact || fact.invalidAt) return;
|
|
205
|
-
|
|
206
|
-
fact.invalidAt = nowIso();
|
|
207
|
-
fact.replacedByFactId = replacedByFactId;
|
|
208
|
-
this.#save();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
currentFacts(spaceId) {
|
|
212
|
-
return cloneData(
|
|
213
|
-
this.data.facts.filter((fact) => fact.spaceId === spaceId && isCurrentFact(fact))
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
listFacts(options = {}) {
|
|
218
|
-
const includeHistorical = options.includeHistorical ?? false;
|
|
219
|
-
return cloneData(
|
|
220
|
-
this.data.facts.filter((fact) => includeHistorical || isCurrentFact(fact))
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
timeline(spaceId, subject) {
|
|
225
|
-
return cloneData(this.data.facts
|
|
226
|
-
.filter((fact) => fact.spaceId === spaceId && fact.subject === subject)
|
|
227
|
-
.sort((left, right) => left.validAt.localeCompare(right.validAt)));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
searchFacts(spaceIds, query, options = {}) {
|
|
231
|
-
return cloneData(searchFileFacts(
|
|
232
|
-
this.data.facts,
|
|
233
|
-
this.#episodeIndex,
|
|
234
|
-
spaceIds,
|
|
235
|
-
query,
|
|
236
|
-
options
|
|
237
|
-
));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
searchFactsPage(spaceIds, query, options = {}) {
|
|
241
|
-
return cloneData(searchFileFactsPage(
|
|
242
|
-
this.data.facts,
|
|
243
|
-
this.#episodeIndex,
|
|
244
|
-
spaceIds,
|
|
245
|
-
query,
|
|
246
|
-
options
|
|
247
|
-
));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
addCandidate(candidate) {
|
|
251
|
-
if (candidate.id && this.data.candidates.some((item) => item.id === candidate.id)) {
|
|
252
|
-
throw new Error(`Duplicate candidate id: ${candidate.id}`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const status = candidate.status ?? CandidateStatus.PENDING;
|
|
256
|
-
const createdAt = candidate.createdAt ?? nowIso();
|
|
257
|
-
const stored = {
|
|
258
|
-
id: candidate.id || randomUUID(),
|
|
259
|
-
personalSpaceId: candidate.personalSpaceId,
|
|
260
|
-
targetSpaceId: candidate.targetSpaceId ?? null,
|
|
261
|
-
episodeId: candidate.episodeId,
|
|
262
|
-
reason: candidate.reason,
|
|
263
|
-
status,
|
|
264
|
-
createdAt,
|
|
265
|
-
decidedAt: candidate.decidedAt ?? (
|
|
266
|
-
status === CandidateStatus.PENDING ? null : createdAt
|
|
267
|
-
)
|
|
268
|
-
};
|
|
269
|
-
validateCandidateRecord(stored);
|
|
270
|
-
this.data.candidates.push(stored);
|
|
271
|
-
this.#save();
|
|
272
|
-
return cloneData(stored);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
pendingCandidates(personalSpaceId) {
|
|
276
|
-
return cloneData(this.data.candidates.filter(
|
|
277
|
-
(candidate) =>
|
|
278
|
-
candidate.personalSpaceId === personalSpaceId &&
|
|
279
|
-
candidate.status === CandidateStatus.PENDING
|
|
280
|
-
));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
getCandidate(id) {
|
|
284
|
-
return cloneData(this.data.candidates.find((candidate) => candidate.id === id) ?? null);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
listCandidates() {
|
|
288
|
-
return cloneData(this.data.candidates);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
updateCandidateStatus(id, status) {
|
|
292
|
-
const candidate = this.data.candidates.find((item) => item.id === id);
|
|
293
|
-
if (!candidate) {
|
|
294
|
-
throw new Error(`Candidate not found: ${id}`);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const updated = {
|
|
298
|
-
...candidate,
|
|
299
|
-
status,
|
|
300
|
-
decidedAt: status === CandidateStatus.PENDING ? null : nowIso()
|
|
301
|
-
};
|
|
302
|
-
validateCandidateRecord(updated);
|
|
303
|
-
Object.assign(candidate, updated);
|
|
304
|
-
this.#save();
|
|
305
|
-
return cloneData(candidate);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
enqueueOutbox(entry) {
|
|
309
|
-
if (entry.id && this.data.outbox.some((item) => item.id === entry.id)) {
|
|
310
|
-
throw new Error(`Duplicate outbox id: ${entry.id}`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const row = {
|
|
314
|
-
id: entry.id || randomUUID(),
|
|
315
|
-
kind: entry.kind,
|
|
316
|
-
aggregateId: entry.aggregateId,
|
|
317
|
-
payload: cloneData(entry.payload),
|
|
318
|
-
status: entry.status ?? 'pending',
|
|
319
|
-
attempts: entry.attempts ?? 0,
|
|
320
|
-
nextAttemptAt: entry.nextAttemptAt ?? null,
|
|
321
|
-
createdAt: entry.createdAt ?? nowIso(),
|
|
322
|
-
sentAt: entry.sentAt ?? null,
|
|
323
|
-
lastError: entry.lastError ?? null
|
|
324
|
-
};
|
|
325
|
-
validateOutboxRecord(row);
|
|
326
|
-
this.data.outbox.push(row);
|
|
327
|
-
this.#save();
|
|
328
|
-
return cloneData(row);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
listPendingOutbox(at = nowIso()) {
|
|
332
|
-
return cloneData(this.data.outbox
|
|
333
|
-
.filter(
|
|
334
|
-
(row) =>
|
|
335
|
-
row.status === 'pending' &&
|
|
336
|
-
(row.nextAttemptAt === null || row.nextAttemptAt <= at)
|
|
337
|
-
)
|
|
338
|
-
.sort((left, right) => left.createdAt.localeCompare(right.createdAt)));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
markOutboxSent(id, sentAt = nowIso()) {
|
|
342
|
-
const row = this.data.outbox.find((item) => item.id === id);
|
|
343
|
-
if (!row) {
|
|
344
|
-
throw new Error(`Outbox row not found: ${id}`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
row.status = 'sent';
|
|
348
|
-
row.nextAttemptAt = null;
|
|
349
|
-
row.sentAt = sentAt;
|
|
350
|
-
row.lastError = null;
|
|
351
|
-
this.#save();
|
|
352
|
-
return cloneData(row);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
markOutboxFailed(id, error, nextAttemptAt = null) {
|
|
356
|
-
const row = this.data.outbox.find((item) => item.id === id);
|
|
357
|
-
if (!row) {
|
|
358
|
-
throw new Error(`Outbox row not found: ${id}`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
row.status = 'pending';
|
|
362
|
-
row.attempts += 1;
|
|
363
|
-
row.nextAttemptAt = nextAttemptAt;
|
|
364
|
-
row.lastError = error;
|
|
365
|
-
this.#save();
|
|
366
|
-
return cloneData(row);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
hasImport(contentHash) {
|
|
370
|
-
return this.data.imports.some((record) => record.contentHash === contentHash);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
recordImport(record) {
|
|
374
|
-
if (this.hasImport(record.contentHash)) {
|
|
375
|
-
throw new Error(`Duplicate import content hash: ${record.contentHash}`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const stored = cloneData(record);
|
|
379
|
-
this.data.imports.push(stored);
|
|
380
|
-
this.#save();
|
|
381
|
-
return cloneData(stored);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
exportSnapshot() {
|
|
385
|
-
return cloneData(this.data);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
importSnapshot(snapshot) {
|
|
389
|
-
this.data = normalizeSnapshot(snapshot, {
|
|
390
|
-
outbox: this.data.outbox,
|
|
391
|
-
imports: this.data.imports
|
|
392
|
-
});
|
|
393
|
-
this.#episodeIndex.rebuild(this.data.episodes);
|
|
394
|
-
this.#save();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
close() {}
|
|
398
|
-
|
|
399
|
-
#load() {
|
|
400
|
-
return normalizeSnapshot(readJsonFile(this.filePath, {}));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
#save() {
|
|
404
|
-
if (this.transactionActive) {
|
|
405
|
-
this.transactionDirty = true;
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
this.#commit();
|
|
411
|
-
} catch (error) {
|
|
412
|
-
this.data = cloneData(this.#committedData);
|
|
413
|
-
this.#episodeIndex.rebuild(this.data.episodes);
|
|
414
|
-
throw error;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
#commit() {
|
|
419
|
-
this.#persist();
|
|
420
|
-
this.#committedData = cloneData(this.data);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
#persist() {
|
|
424
|
-
if (this.memoryOnly) return;
|
|
425
|
-
writeJsonFileAtomic(this.filePath, this.data, this.jsonFileIo);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function cloneData(data) {
|
|
430
|
-
return JSON.parse(JSON.stringify(data));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function isThenable(value) {
|
|
434
|
-
return value !== null &&
|
|
435
|
-
(typeof value === 'object' || typeof value === 'function') &&
|
|
436
|
-
typeof value.then === 'function';
|
|
437
|
-
}
|
package/src/storage/migrate.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { nowIso } from '../models.js';
|
|
6
|
-
import { isFileDatabase } from './sqlite/open-database.js';
|
|
7
|
-
|
|
8
|
-
const DEFAULT_MIGRATIONS_DIRECTORY = new URL('./migrations/', import.meta.url);
|
|
9
|
-
const CHECKSUM_UPGRADE_MIGRATION = '002-storage-integrity.sql';
|
|
10
|
-
|
|
11
|
-
export function configureDatabase(db, filePath) {
|
|
12
|
-
db.pragma('foreign_keys = ON');
|
|
13
|
-
db.pragma('busy_timeout = 5000');
|
|
14
|
-
if (isFileDatabase(filePath)) db.pragma('journal_mode = WAL');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function runMigrations(
|
|
18
|
-
db,
|
|
19
|
-
{ migrationsDirectory = DEFAULT_MIGRATIONS_DIRECTORY } = {}
|
|
20
|
-
) {
|
|
21
|
-
db.pragma('foreign_keys = ON');
|
|
22
|
-
const migrations = discoverMigrations(migrationsDirectory);
|
|
23
|
-
for (const migration of migrations) {
|
|
24
|
-
const migrate = db.transaction(() => {
|
|
25
|
-
const state = appliedMigrations(db);
|
|
26
|
-
verifyAppliedMigrations(migrations, state.records, {
|
|
27
|
-
allowMissingChecksums: hasPendingChecksumUpgrade(migrations, state)
|
|
28
|
-
});
|
|
29
|
-
if (state.records.has(migration.version)) return;
|
|
30
|
-
db.exec(migration.sql);
|
|
31
|
-
recordMigration(db, migration);
|
|
32
|
-
});
|
|
33
|
-
migrate.immediate();
|
|
34
|
-
}
|
|
35
|
-
verifyAppliedMigrations(migrations, appliedMigrations(db).records);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function discoverMigrations(directory) {
|
|
39
|
-
const migrations = readdirSync(directory)
|
|
40
|
-
.filter((name) => name.endsWith('.sql'))
|
|
41
|
-
.map((name) => {
|
|
42
|
-
const match = /^(\d+)-.+\.sql$/.exec(name);
|
|
43
|
-
if (!match) throw new Error(`Invalid migration filename: ${name}`);
|
|
44
|
-
const sql = readFileSync(migrationPath(directory, name), 'utf8');
|
|
45
|
-
return {
|
|
46
|
-
version: Number.parseInt(match[1], 10),
|
|
47
|
-
name,
|
|
48
|
-
sql,
|
|
49
|
-
checksum: createHash('sha256').update(sql).digest('hex')
|
|
50
|
-
};
|
|
51
|
-
})
|
|
52
|
-
.sort((left, right) => left.version - right.version || left.name.localeCompare(right.name));
|
|
53
|
-
|
|
54
|
-
for (let index = 1; index < migrations.length; index += 1) {
|
|
55
|
-
if (migrations[index - 1].version === migrations[index].version) {
|
|
56
|
-
throw new Error(`Duplicate migration version: ${migrations[index].version}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return migrations;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function appliedMigrations(db) {
|
|
63
|
-
const columns = db.prepare('PRAGMA table_info(schema_migrations)').all();
|
|
64
|
-
if (columns.length === 0) return { records: new Map(), supportsChecksums: false };
|
|
65
|
-
|
|
66
|
-
const supportsChecksums = columns.some((column) => column.name === 'checksum');
|
|
67
|
-
const rows = supportsChecksums
|
|
68
|
-
? db.prepare(`
|
|
69
|
-
SELECT version, checksum FROM schema_migrations ORDER BY version
|
|
70
|
-
`).all()
|
|
71
|
-
: db.prepare(`
|
|
72
|
-
SELECT version, NULL AS checksum FROM schema_migrations ORDER BY version
|
|
73
|
-
`).all();
|
|
74
|
-
return {
|
|
75
|
-
records: new Map(rows.map((row) => [row.version, row.checksum])),
|
|
76
|
-
supportsChecksums
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function verifyAppliedMigrations(
|
|
81
|
-
migrations,
|
|
82
|
-
applied,
|
|
83
|
-
{ allowMissingChecksums = false } = {}
|
|
84
|
-
) {
|
|
85
|
-
const available = new Map(migrations.map((migration) => [migration.version, migration]));
|
|
86
|
-
for (const [version, checksum] of applied) {
|
|
87
|
-
const migration = available.get(version);
|
|
88
|
-
if (!migration) {
|
|
89
|
-
throw new Error(
|
|
90
|
-
`Applied migration version ${version} is missing from the migration directory`
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
if (checksum === null && allowMissingChecksums) continue;
|
|
94
|
-
if (checksum === null) {
|
|
95
|
-
throw new Error(`Applied migration version ${version} is missing a checksum`);
|
|
96
|
-
}
|
|
97
|
-
if (migration.checksum !== checksum) {
|
|
98
|
-
throw new Error(`Migration checksum mismatch for version ${version}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function hasPendingChecksumUpgrade(migrations, state) {
|
|
104
|
-
if (state.supportsChecksums) return false;
|
|
105
|
-
const migration = migrations.find(
|
|
106
|
-
(candidate) => candidate.name === CHECKSUM_UPGRADE_MIGRATION
|
|
107
|
-
);
|
|
108
|
-
return migration !== undefined && !state.records.has(migration.version);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function recordMigration(db, migration) {
|
|
112
|
-
const supportsChecksums = db.prepare('PRAGMA table_info(schema_migrations)').all()
|
|
113
|
-
.some((column) => column.name === 'checksum');
|
|
114
|
-
if (supportsChecksums) {
|
|
115
|
-
db.prepare(`
|
|
116
|
-
INSERT INTO schema_migrations (version, checksum, applied_at)
|
|
117
|
-
VALUES (?, ?, ?)
|
|
118
|
-
`).run(migration.version, migration.checksum, nowIso());
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
db.prepare(`
|
|
122
|
-
INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)
|
|
123
|
-
`).run(migration.version, nowIso());
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function migrationPath(directory, name) {
|
|
127
|
-
return directory instanceof URL ? new URL(name, directory) : join(directory, name);
|
|
128
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
PRAGMA foreign_keys = ON;
|
|
2
|
-
|
|
3
|
-
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
4
|
-
version INTEGER PRIMARY KEY,
|
|
5
|
-
applied_at TEXT NOT NULL
|
|
6
|
-
);
|
|
7
|
-
|
|
8
|
-
CREATE TABLE spaces (
|
|
9
|
-
id TEXT PRIMARY KEY,
|
|
10
|
-
name TEXT NOT NULL UNIQUE,
|
|
11
|
-
kind TEXT NOT NULL CHECK (kind IN ('personal', 'public')),
|
|
12
|
-
description TEXT,
|
|
13
|
-
created_at TEXT NOT NULL
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
CREATE TABLE subscriptions (
|
|
17
|
-
personal_space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
18
|
-
space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
19
|
-
mode TEXT NOT NULL DEFAULT 'latest',
|
|
20
|
-
created_at TEXT NOT NULL,
|
|
21
|
-
PRIMARY KEY (personal_space_id, space_id)
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
CREATE TABLE episodes (
|
|
25
|
-
id TEXT PRIMARY KEY,
|
|
26
|
-
space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
27
|
-
source_kind TEXT NOT NULL,
|
|
28
|
-
body TEXT NOT NULL,
|
|
29
|
-
source_uri TEXT,
|
|
30
|
-
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
31
|
-
created_at TEXT NOT NULL
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
CREATE TABLE facts (
|
|
35
|
-
id TEXT PRIMARY KEY,
|
|
36
|
-
space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
37
|
-
subject TEXT NOT NULL,
|
|
38
|
-
predicate TEXT NOT NULL,
|
|
39
|
-
object TEXT NOT NULL,
|
|
40
|
-
source_episode_id TEXT NOT NULL REFERENCES episodes(id),
|
|
41
|
-
status TEXT NOT NULL,
|
|
42
|
-
confidence REAL NOT NULL DEFAULT 1.0,
|
|
43
|
-
sensitivity TEXT NOT NULL DEFAULT 'normal',
|
|
44
|
-
scope TEXT NOT NULL DEFAULT 'personal',
|
|
45
|
-
valid_at TEXT NOT NULL,
|
|
46
|
-
invalid_at TEXT,
|
|
47
|
-
replaced_by_fact_id TEXT REFERENCES facts(id)
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
CREATE INDEX facts_current_idx ON facts(space_id, predicate, invalid_at);
|
|
51
|
-
CREATE INDEX facts_source_idx ON facts(source_episode_id);
|
|
52
|
-
|
|
53
|
-
CREATE TABLE candidates (
|
|
54
|
-
id TEXT PRIMARY KEY,
|
|
55
|
-
personal_space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
56
|
-
target_space_id TEXT REFERENCES spaces(id),
|
|
57
|
-
episode_id TEXT NOT NULL REFERENCES episodes(id),
|
|
58
|
-
reason TEXT NOT NULL,
|
|
59
|
-
status TEXT NOT NULL,
|
|
60
|
-
created_at TEXT NOT NULL,
|
|
61
|
-
decided_at TEXT
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
CREATE TABLE outbox (
|
|
65
|
-
id TEXT PRIMARY KEY,
|
|
66
|
-
kind TEXT NOT NULL,
|
|
67
|
-
aggregate_id TEXT NOT NULL,
|
|
68
|
-
payload_json TEXT NOT NULL,
|
|
69
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
70
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
71
|
-
next_attempt_at TEXT,
|
|
72
|
-
created_at TEXT NOT NULL,
|
|
73
|
-
sent_at TEXT,
|
|
74
|
-
last_error TEXT
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
CREATE INDEX outbox_pending_idx ON outbox(status, next_attempt_at, created_at);
|
|
78
|
-
|
|
79
|
-
CREATE TABLE imports (
|
|
80
|
-
content_hash TEXT PRIMARY KEY,
|
|
81
|
-
source_path TEXT NOT NULL,
|
|
82
|
-
imported_at TEXT NOT NULL
|
|
83
|
-
);
|