fuli-context 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/compose.graphiti.yml +86 -0
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
- package/dist/web/assets/console-tbvTUi6p.js +1 -0
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-B3s5CXhv.js +1 -0
- package/dist/web/assets/index-Devf7glZ.css +1 -0
- package/dist/web/assets/index-DwUDhQKc.js +6 -0
- package/dist/web/assets/model-DVT8sjy9.js +1 -0
- package/dist/web/assets/paths-B790iniO.js +1 -0
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
- package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
- package/dist/web/index.html +22 -0
- package/graph-provider/Dockerfile +16 -0
- package/graph-provider/fuli_graph/__init__.py +5 -0
- package/graph-provider/fuli_graph/app.py +547 -0
- package/graph-provider/fuli_graph/auth.py +17 -0
- package/graph-provider/fuli_graph/collaboration_context.py +290 -0
- package/graph-provider/fuli_graph/config.py +34 -0
- package/graph-provider/fuli_graph/graph_models.py +123 -0
- package/graph-provider/fuli_graph/graph_projection.py +405 -0
- package/graph-provider/fuli_graph/graph_query.py +561 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
- package/graph-provider/fuli_graph/knowledge_management.py +816 -0
- package/graph-provider/fuli_graph/knowledge_records.py +65 -0
- package/graph-provider/fuli_graph/knowledge_search.py +723 -0
- package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
- package/graph-provider/fuli_graph/model_validation.py +58 -0
- package/graph-provider/fuli_graph/models.py +1179 -0
- package/graph-provider/fuli_graph/personal_project_access.py +29 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
- package/graph-provider/fuli_graph/project_action_models.py +142 -0
- package/graph-provider/fuli_graph/project_knowledge.py +707 -0
- package/graph-provider/fuli_graph/project_management.py +251 -0
- package/graph-provider/fuli_graph/provider_values.py +40 -0
- package/graph-provider/fuli_graph/runtime.py +350 -0
- package/graph-provider/fuli_graph/search_projection.py +156 -0
- package/graph-provider/fuli_graph/store.py +24 -0
- package/graph-provider/fuli_graph/store_identity.py +237 -0
- package/graph-provider/fuli_graph/store_knowledge.py +447 -0
- package/graph-provider/fuli_graph/store_projects.py +242 -0
- package/graph-provider/fuli_graph/store_publication.py +370 -0
- package/graph-provider/fuli_graph/store_records.py +134 -0
- package/graph-provider/pyproject.toml +35 -0
- package/npm-shrinkwrap.json +6045 -0
- package/package.json +99 -0
- package/skills/capturing-session-knowledge/SKILL.md +226 -0
- package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
- package/skills/grilling-project/SKILL.md +129 -0
- package/skills/grilling-project/agents/openai.yaml +4 -0
- package/skills/grilling-project/references/project-profile.md +105 -0
- package/src/agent/candidate-read-service.js +53 -0
- package/src/agent/context-pack-service.js +83 -0
- package/src/agent/public-read-policy.js +39 -0
- package/src/agent/public-read-service.js +69 -0
- package/src/agent/read-projection.js +107 -0
- package/src/agent/search-service.js +61 -0
- package/src/agent/space-boundary.js +33 -0
- package/src/agent-tools/graph-definitions.js +479 -0
- package/src/agent-tools/graph-handlers.js +96 -0
- package/src/agent-tools/schema.js +37 -0
- package/src/agent-tools.js +16 -0
- package/src/app/application-error.js +13 -0
- package/src/app/bootstrap.js +8 -0
- package/src/app/create-application.js +143 -0
- package/src/app/state-service.js +38 -0
- package/src/candidates.js +78 -0
- package/src/classifier.js +63 -0
- package/src/cli/command-arguments.js +14 -0
- package/src/cli/command-registry.js +78 -0
- package/src/cli/commands/candidates.js +17 -0
- package/src/cli/commands/ingestion.js +32 -0
- package/src/cli/commands/reads.js +50 -0
- package/src/cli/commands/spaces.js +20 -0
- package/src/cli/invocation.js +47 -0
- package/src/cli/local-runtime-command.js +94 -0
- package/src/cli/local-runtime-options.js +64 -0
- package/src/cli/migrate-command.js +71 -0
- package/src/cli/migration-destination.js +117 -0
- package/src/cli/setup-agent-selection.js +195 -0
- package/src/cli/setup-command.js +85 -0
- package/src/cli/uninstall-command.js +67 -0
- package/src/cli.js +90 -0
- package/src/context-pack.js +206 -0
- package/src/defaults.js +1 -0
- package/src/extractor.js +77 -0
- package/src/fact-history.js +39 -0
- package/src/facts/fact-service.js +89 -0
- package/src/graphiti/agent-access-policy.js +88 -0
- package/src/graphiti/capture-policy.js +84 -0
- package/src/graphiti/federated-application.js +1292 -0
- package/src/graphiti/personal-context.js +29 -0
- package/src/graphiti/project-path-context.js +159 -0
- package/src/graphiti/provider-client.js +288 -0
- package/src/graphiti/runtime-config.js +85 -0
- package/src/graphiti/setup.js +43 -0
- package/src/graphiti/source-marker.js +142 -0
- package/src/http/api-router.js +133 -0
- package/src/http/bounded-json-array.js +19 -0
- package/src/http/error-mapping.js +36 -0
- package/src/http/graph-api-router.js +257 -0
- package/src/http/lens-route.js +50 -0
- package/src/http/request-policy.js +41 -0
- package/src/http/response.js +29 -0
- package/src/http/static-handler.js +55 -0
- package/src/ingestion.js +161 -0
- package/src/lens/lens-correction-service.js +81 -0
- package/src/lens/lens-fact-writer.js +85 -0
- package/src/lens/lens-input.js +62 -0
- package/src/lens/lens-query-projection.js +87 -0
- package/src/lens/lens-query-ranking.js +62 -0
- package/src/lens/lens-query.js +143 -0
- package/src/lens/lens-resource-projection.js +93 -0
- package/src/lens/lens-resource-selection.js +45 -0
- package/src/lens/lens-resource-service.js +103 -0
- package/src/lens/lens-search-query.js +122 -0
- package/src/lens/lens-service.js +183 -0
- package/src/local-runtime/lifecycle.js +254 -0
- package/src/mcp/create-mcp-server.js +85 -0
- package/src/mcp/instructions.js +10 -0
- package/src/mcp/project-action-preview-tokens.js +72 -0
- package/src/mcp/runtime.js +71 -0
- package/src/mcp/tool-annotations.js +63 -0
- package/src/mcp/tool-result.js +167 -0
- package/src/mcp/tool-schema.js +60 -0
- package/src/mcp-server.js +76 -0
- package/src/models.js +44 -0
- package/src/observer.js +83 -0
- package/src/package-metadata.js +6 -0
- package/src/project-rules.js +33 -0
- package/src/publication/canonical-json.js +39 -0
- package/src/publication/outbox-service.js +46 -0
- package/src/publication/publication-envelope-schema.js +75 -0
- package/src/publication/publication-envelope.js +37 -0
- package/src/publication/publication-policy.js +82 -0
- package/src/publication/publication-service.js +48 -0
- package/src/router.js +58 -0
- package/src/runtime-options.js +139 -0
- package/src/security/episode-visibility.js +14 -0
- package/src/security/fact-visibility.js +15 -0
- package/src/security/sensitive-content.js +18 -0
- package/src/security/source-metadata-policy.js +8 -0
- package/src/server/application-lifecycle.js +34 -0
- package/src/server/listen.js +24 -0
- package/src/server.js +135 -0
- package/src/setup/agent-installation-status.js +97 -0
- package/src/setup/agents.js +123 -0
- package/src/setup/codex-bootstrap.js +118 -0
- package/src/setup/codex-config.js +122 -0
- package/src/setup/config-backup.js +22 -0
- package/src/setup/container-runtime.js +388 -0
- package/src/setup/cursor-config.js +61 -0
- package/src/setup/graph-runtime.js +360 -0
- package/src/setup/node-runtime.js +24 -0
- package/src/setup/options.js +91 -0
- package/src/setup/paths.js +47 -0
- package/src/setup/session-skill.js +91 -0
- package/src/setup/setup.js +85 -0
- package/src/setup/uninstall.js +90 -0
- package/src/storage/episode-evidence-preview.js +100 -0
- package/src/storage/fact-search-order.js +18 -0
- package/src/storage/file/episode-evidence.js +37 -0
- package/src/storage/file/episode-index.js +59 -0
- package/src/storage/file/fact-search.js +60 -0
- package/src/storage/file-store.js +437 -0
- package/src/storage/import-json.js +70 -0
- package/src/storage/json-file.js +41 -0
- package/src/storage/legacy-record-validation.js +152 -0
- package/src/storage/legacy-snapshot-validation.js +32 -0
- package/src/storage/migrate.js +128 -0
- package/src/storage/migrations/001-initial.sql +83 -0
- package/src/storage/migrations/002-storage-integrity.sql +113 -0
- package/src/storage/record-validation.js +46 -0
- package/src/storage/snapshot-defaults.js +54 -0
- package/src/storage/sqlite/candidates-repository.js +84 -0
- package/src/storage/sqlite/episode-evidence-repository.js +154 -0
- package/src/storage/sqlite/episode-privacy-sql.js +33 -0
- package/src/storage/sqlite/episodes-repository.js +52 -0
- package/src/storage/sqlite/fact-search-repository.js +162 -0
- package/src/storage/sqlite/facts-repository.js +146 -0
- package/src/storage/sqlite/imports-repository.js +39 -0
- package/src/storage/sqlite/mapper.js +84 -0
- package/src/storage/sqlite/open-database.js +15 -0
- package/src/storage/sqlite/outbox-repository.js +103 -0
- package/src/storage/sqlite/snapshot-repository.js +69 -0
- package/src/storage/sqlite/spaces-repository.js +52 -0
- package/src/storage/sqlite/subscriptions-repository.js +54 -0
- package/src/storage/sqlite-store.js +159 -0
- package/src/storage/stable-json.js +24 -0
- package/src/storage/store-port.js +20 -0
- package/src/store.js +2 -0
- package/src/workspace-protocol/array-validation.js +19 -0
- package/src/workspace-protocol/base64url-contract.js +27 -0
- package/src/workspace-protocol/constants.js +37 -0
- package/src/workspace-protocol/error-contract.js +24 -0
- package/src/workspace-protocol/fact-contract.js +110 -0
- package/src/workspace-protocol/governance-contract.js +47 -0
- package/src/workspace-protocol/index.js +53 -0
- package/src/workspace-protocol/json-budget.js +19 -0
- package/src/workspace-protocol/manifest-contract.js +186 -0
- package/src/workspace-protocol/publication-contract.js +212 -0
- package/src/workspace-protocol/query-contract.js +149 -0
- package/src/workspace-protocol/scalar-contract.js +192 -0
- package/src/workspace-protocol/sync-contract.js +184 -0
- package/src/workspace-protocol/text-contract.js +39 -0
- package/src/workspace-protocol/validation.js +66 -0
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { FactStatus, nowIso } from '../../models.js';
|
|
4
|
+
import { validateFactRecord } from '../record-validation.js';
|
|
5
|
+
import { FactSearchRepository } from './fact-search-repository.js';
|
|
6
|
+
import { mapFact } from './mapper.js';
|
|
7
|
+
|
|
8
|
+
export class FactsRepository {
|
|
9
|
+
constructor(db) {
|
|
10
|
+
this.factSearch = new FactSearchRepository(db);
|
|
11
|
+
this.insertStatement = db.prepare(`
|
|
12
|
+
INSERT INTO facts (
|
|
13
|
+
id, space_id, subject, predicate, object, source_episode_id, status,
|
|
14
|
+
confidence, sensitivity, scope, valid_at, invalid_at, replaced_by_fact_id
|
|
15
|
+
) VALUES (
|
|
16
|
+
@id, @spaceId, @subject, @predicate, @object, @sourceEpisodeId, @status,
|
|
17
|
+
@confidence, @sensitivity, @scope, @validAt, @invalidAt, @replacedByFactId
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
this.getStatement = db.prepare('SELECT * FROM facts WHERE id = ?');
|
|
21
|
+
this.updateStatement = db.prepare(`
|
|
22
|
+
UPDATE facts SET
|
|
23
|
+
space_id = @spaceId,
|
|
24
|
+
subject = @subject,
|
|
25
|
+
predicate = @predicate,
|
|
26
|
+
object = @object,
|
|
27
|
+
source_episode_id = @sourceEpisodeId,
|
|
28
|
+
status = @status,
|
|
29
|
+
confidence = @confidence,
|
|
30
|
+
sensitivity = @sensitivity,
|
|
31
|
+
scope = @scope,
|
|
32
|
+
valid_at = @validAt,
|
|
33
|
+
invalid_at = @invalidAt,
|
|
34
|
+
replaced_by_fact_id = @replacedByFactId
|
|
35
|
+
WHERE id = @id
|
|
36
|
+
`);
|
|
37
|
+
this.currentStatement = db.prepare(`
|
|
38
|
+
SELECT * FROM facts
|
|
39
|
+
WHERE space_id = ?
|
|
40
|
+
AND invalid_at IS NULL
|
|
41
|
+
AND status NOT IN ('rejected', 'deprecated')
|
|
42
|
+
ORDER BY rowid
|
|
43
|
+
`);
|
|
44
|
+
this.currentListStatement = db.prepare(`
|
|
45
|
+
SELECT * FROM facts
|
|
46
|
+
WHERE invalid_at IS NULL AND status NOT IN ('rejected', 'deprecated')
|
|
47
|
+
ORDER BY rowid
|
|
48
|
+
`);
|
|
49
|
+
this.fullListStatement = db.prepare('SELECT * FROM facts ORDER BY rowid');
|
|
50
|
+
this.timelineStatement = db.prepare(`
|
|
51
|
+
SELECT * FROM facts
|
|
52
|
+
WHERE space_id = ? AND subject = ?
|
|
53
|
+
ORDER BY valid_at, rowid
|
|
54
|
+
`);
|
|
55
|
+
this.replaceLinkStatement = db.prepare(`
|
|
56
|
+
UPDATE facts SET replaced_by_fact_id = ? WHERE id = ?
|
|
57
|
+
`);
|
|
58
|
+
this.deleteStatement = db.prepare('DELETE FROM facts');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
add(fact) {
|
|
62
|
+
if (fact.id && this.get(fact.id)) {
|
|
63
|
+
throw new Error(`Duplicate fact id: ${fact.id}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stored = {
|
|
67
|
+
id: fact.id || randomUUID(),
|
|
68
|
+
spaceId: fact.spaceId,
|
|
69
|
+
subject: fact.subject,
|
|
70
|
+
predicate: fact.predicate,
|
|
71
|
+
object: fact.object,
|
|
72
|
+
sourceEpisodeId: fact.sourceEpisodeId,
|
|
73
|
+
status: fact.status ?? FactStatus.CONFIRMED,
|
|
74
|
+
confidence: fact.confidence ?? 1,
|
|
75
|
+
sensitivity: fact.sensitivity ?? 'normal',
|
|
76
|
+
scope: fact.scope ?? 'personal',
|
|
77
|
+
validAt: fact.validAt ?? nowIso(),
|
|
78
|
+
invalidAt: fact.invalidAt ?? null,
|
|
79
|
+
replacedByFactId: fact.replacedByFactId ?? null
|
|
80
|
+
};
|
|
81
|
+
validateFactRecord(stored);
|
|
82
|
+
this.insertStatement.run(stored);
|
|
83
|
+
return this.get(stored.id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get(id) {
|
|
87
|
+
return mapFact(this.getStatement.get(id));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
update(id, updates) {
|
|
91
|
+
const existing = this.get(id);
|
|
92
|
+
if (!existing) throw new Error(`Fact not found: ${id}`);
|
|
93
|
+
|
|
94
|
+
const stored = { ...existing, ...updates, id };
|
|
95
|
+
validateFactRecord(stored);
|
|
96
|
+
this.updateStatement.run(stored);
|
|
97
|
+
return this.get(id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
invalidate(id, replacedByFactId = null) {
|
|
101
|
+
const fact = this.get(id);
|
|
102
|
+
if (!fact || fact.invalidAt) return;
|
|
103
|
+
this.updateStatement.run({
|
|
104
|
+
...fact,
|
|
105
|
+
invalidAt: nowIso(),
|
|
106
|
+
replacedByFactId
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
current(spaceId) {
|
|
111
|
+
return this.currentStatement.all(spaceId).map(mapFact);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
list({ includeHistorical = false } = {}) {
|
|
115
|
+
const statement = includeHistorical ? this.fullListStatement : this.currentListStatement;
|
|
116
|
+
return statement.all().map(mapFact);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
timeline(spaceId, subject) {
|
|
120
|
+
return this.timelineStatement.all(spaceId, subject).map(mapFact);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
search(spaceIds, query, options = {}) {
|
|
124
|
+
return this.factSearch.search(spaceIds, query, options);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
searchPage(spaceIds, query, options = {}) {
|
|
128
|
+
return this.factSearch.searchPage(spaceIds, query, options);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
insertSnapshot(fact, { deferReplacement = false } = {}) {
|
|
132
|
+
validateFactRecord(fact);
|
|
133
|
+
this.insertStatement.run({
|
|
134
|
+
...fact,
|
|
135
|
+
replacedByFactId: deferReplacement ? null : fact.replacedByFactId
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
restoreReplacement(id, replacedByFactId) {
|
|
140
|
+
this.replaceLinkStatement.run(replacedByFactId, id);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
deleteAll() {
|
|
144
|
+
this.deleteStatement.run();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mapImport } from './mapper.js';
|
|
2
|
+
|
|
3
|
+
export class ImportsRepository {
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.insertStatement = db.prepare(`
|
|
6
|
+
INSERT INTO imports (content_hash, source_path, imported_at)
|
|
7
|
+
VALUES (@contentHash, @sourcePath, @importedAt)
|
|
8
|
+
`);
|
|
9
|
+
this.hasStatement = db.prepare(`
|
|
10
|
+
SELECT 1 FROM imports WHERE content_hash = ?
|
|
11
|
+
`);
|
|
12
|
+
this.listStatement = db.prepare('SELECT * FROM imports ORDER BY rowid');
|
|
13
|
+
this.deleteStatement = db.prepare('DELETE FROM imports');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
has(contentHash) {
|
|
17
|
+
return this.hasStatement.get(contentHash) !== undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
record(record) {
|
|
21
|
+
if (this.has(record.contentHash)) {
|
|
22
|
+
throw new Error(`Duplicate import content hash: ${record.contentHash}`);
|
|
23
|
+
}
|
|
24
|
+
this.insertStatement.run(record);
|
|
25
|
+
return { ...record };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
listAll() {
|
|
29
|
+
return this.listStatement.all().map(mapImport);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
insertSnapshot(record) {
|
|
33
|
+
this.insertStatement.run(record);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
deleteAll() {
|
|
37
|
+
this.deleteStatement.run();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export function mapSpace(row) {
|
|
2
|
+
return row ? {
|
|
3
|
+
id: row.id,
|
|
4
|
+
name: row.name,
|
|
5
|
+
kind: row.kind,
|
|
6
|
+
description: row.description,
|
|
7
|
+
createdAt: row.created_at
|
|
8
|
+
} : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function mapSubscription(row) {
|
|
12
|
+
return row ? {
|
|
13
|
+
personalSpaceId: row.personal_space_id,
|
|
14
|
+
spaceId: row.space_id,
|
|
15
|
+
mode: row.mode,
|
|
16
|
+
createdAt: row.created_at
|
|
17
|
+
} : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mapEpisode(row) {
|
|
21
|
+
return row ? {
|
|
22
|
+
id: row.id,
|
|
23
|
+
spaceId: row.space_id,
|
|
24
|
+
sourceKind: row.source_kind,
|
|
25
|
+
body: row.body,
|
|
26
|
+
sourceUri: row.source_uri,
|
|
27
|
+
metadata: JSON.parse(row.metadata_json),
|
|
28
|
+
createdAt: row.created_at
|
|
29
|
+
} : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mapFact(row) {
|
|
33
|
+
return row ? {
|
|
34
|
+
id: row.id,
|
|
35
|
+
spaceId: row.space_id,
|
|
36
|
+
subject: row.subject,
|
|
37
|
+
predicate: row.predicate,
|
|
38
|
+
object: row.object,
|
|
39
|
+
sourceEpisodeId: row.source_episode_id,
|
|
40
|
+
status: row.status,
|
|
41
|
+
confidence: row.confidence,
|
|
42
|
+
sensitivity: row.sensitivity,
|
|
43
|
+
scope: row.scope,
|
|
44
|
+
validAt: row.valid_at,
|
|
45
|
+
invalidAt: row.invalid_at,
|
|
46
|
+
replacedByFactId: row.replaced_by_fact_id
|
|
47
|
+
} : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mapCandidate(row) {
|
|
51
|
+
return row ? {
|
|
52
|
+
id: row.id,
|
|
53
|
+
personalSpaceId: row.personal_space_id,
|
|
54
|
+
targetSpaceId: row.target_space_id,
|
|
55
|
+
episodeId: row.episode_id,
|
|
56
|
+
reason: row.reason,
|
|
57
|
+
status: row.status,
|
|
58
|
+
createdAt: row.created_at,
|
|
59
|
+
decidedAt: row.decided_at
|
|
60
|
+
} : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function mapOutbox(row) {
|
|
64
|
+
return row ? {
|
|
65
|
+
id: row.id,
|
|
66
|
+
kind: row.kind,
|
|
67
|
+
aggregateId: row.aggregate_id,
|
|
68
|
+
payload: JSON.parse(row.payload_json),
|
|
69
|
+
status: row.status,
|
|
70
|
+
attempts: row.attempts,
|
|
71
|
+
nextAttemptAt: row.next_attempt_at,
|
|
72
|
+
createdAt: row.created_at,
|
|
73
|
+
sentAt: row.sent_at,
|
|
74
|
+
lastError: row.last_error
|
|
75
|
+
} : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mapImport(row) {
|
|
79
|
+
return row ? {
|
|
80
|
+
contentHash: row.content_hash,
|
|
81
|
+
sourcePath: row.source_path,
|
|
82
|
+
importedAt: row.imported_at
|
|
83
|
+
} : null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
|
|
6
|
+
export function openDatabase(filePath) {
|
|
7
|
+
if (isFileDatabase(filePath)) {
|
|
8
|
+
mkdirSync(dirname(resolve(filePath)), { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
return new Database(filePath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isFileDatabase(filePath) {
|
|
14
|
+
return typeof filePath === 'string' && filePath !== ':memory:' && filePath !== '';
|
|
15
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { nowIso } from '../../models.js';
|
|
4
|
+
import { validateOutboxRecord } from '../record-validation.js';
|
|
5
|
+
import { mapOutbox } from './mapper.js';
|
|
6
|
+
|
|
7
|
+
export class OutboxRepository {
|
|
8
|
+
constructor(db) {
|
|
9
|
+
this.insertStatement = db.prepare(`
|
|
10
|
+
INSERT INTO outbox (
|
|
11
|
+
id, kind, aggregate_id, payload_json, status, attempts,
|
|
12
|
+
next_attempt_at, created_at, sent_at, last_error
|
|
13
|
+
) VALUES (
|
|
14
|
+
@id, @kind, @aggregateId, @payloadJson, @status, @attempts,
|
|
15
|
+
@nextAttemptAt, @createdAt, @sentAt, @lastError
|
|
16
|
+
)
|
|
17
|
+
`);
|
|
18
|
+
this.getStatement = db.prepare('SELECT * FROM outbox WHERE id = ?');
|
|
19
|
+
this.listStatement = db.prepare('SELECT * FROM outbox ORDER BY rowid');
|
|
20
|
+
this.pendingStatement = db.prepare(`
|
|
21
|
+
SELECT * FROM outbox
|
|
22
|
+
WHERE status = 'pending'
|
|
23
|
+
AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
24
|
+
ORDER BY created_at, rowid
|
|
25
|
+
`);
|
|
26
|
+
this.markSentStatement = db.prepare(`
|
|
27
|
+
UPDATE outbox SET
|
|
28
|
+
status = 'sent', next_attempt_at = NULL, sent_at = ?, last_error = NULL
|
|
29
|
+
WHERE id = ?
|
|
30
|
+
`);
|
|
31
|
+
this.markFailedStatement = db.prepare(`
|
|
32
|
+
UPDATE outbox SET
|
|
33
|
+
status = 'pending', attempts = attempts + 1,
|
|
34
|
+
next_attempt_at = ?, last_error = ?
|
|
35
|
+
WHERE id = ?
|
|
36
|
+
`);
|
|
37
|
+
this.deleteStatement = db.prepare('DELETE FROM outbox');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
enqueue(entry) {
|
|
41
|
+
if (entry.id && this.get(entry.id)) {
|
|
42
|
+
throw new Error(`Duplicate outbox id: ${entry.id}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stored = {
|
|
46
|
+
id: entry.id || randomUUID(),
|
|
47
|
+
kind: entry.kind,
|
|
48
|
+
aggregateId: entry.aggregateId,
|
|
49
|
+
payloadJson: JSON.stringify(entry.payload),
|
|
50
|
+
status: entry.status ?? 'pending',
|
|
51
|
+
attempts: entry.attempts ?? 0,
|
|
52
|
+
nextAttemptAt: entry.nextAttemptAt ?? null,
|
|
53
|
+
createdAt: entry.createdAt ?? nowIso(),
|
|
54
|
+
sentAt: entry.sentAt ?? null,
|
|
55
|
+
lastError: entry.lastError ?? null
|
|
56
|
+
};
|
|
57
|
+
validateOutboxRecord(stored);
|
|
58
|
+
this.insertStatement.run(stored);
|
|
59
|
+
return this.get(stored.id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
listPending(at = nowIso()) {
|
|
63
|
+
return this.pendingStatement.all(at).map(mapOutbox);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
markSent(id, sentAt = nowIso()) {
|
|
67
|
+
this.require(id);
|
|
68
|
+
this.markSentStatement.run(sentAt, id);
|
|
69
|
+
return this.get(id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
markFailed(id, error, nextAttemptAt = null) {
|
|
73
|
+
this.require(id);
|
|
74
|
+
this.markFailedStatement.run(nextAttemptAt, error, id);
|
|
75
|
+
return this.get(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get(id) {
|
|
79
|
+
return mapOutbox(this.getStatement.get(id));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
listAll() {
|
|
83
|
+
return this.listStatement.all().map(mapOutbox);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
insertSnapshot(entry) {
|
|
87
|
+
validateOutboxRecord(entry);
|
|
88
|
+
this.insertStatement.run({
|
|
89
|
+
...entry,
|
|
90
|
+
payloadJson: JSON.stringify(entry.payload)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
deleteAll() {
|
|
95
|
+
this.deleteStatement.run();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
require(id) {
|
|
99
|
+
const row = this.get(id);
|
|
100
|
+
if (!row) throw new Error(`Outbox row not found: ${id}`);
|
|
101
|
+
return row;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { cloneSnapshot, normalizeSnapshot } from '../snapshot-defaults.js';
|
|
2
|
+
|
|
3
|
+
export class SnapshotRepository {
|
|
4
|
+
constructor(db, repositories) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
this.repositories = repositories;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export() {
|
|
10
|
+
const repositories = this.repositories;
|
|
11
|
+
return cloneSnapshot({
|
|
12
|
+
spaces: repositories.spaces.list(),
|
|
13
|
+
subscriptions: repositories.subscriptions.list(),
|
|
14
|
+
episodes: repositories.episodes.list(),
|
|
15
|
+
facts: repositories.facts.list({ includeHistorical: true }),
|
|
16
|
+
candidates: repositories.candidates.list(),
|
|
17
|
+
outbox: repositories.outbox.listAll(),
|
|
18
|
+
imports: repositories.imports.listAll()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
import(snapshot) {
|
|
23
|
+
const current = this.export();
|
|
24
|
+
const normalized = normalizeSnapshot(snapshot, {
|
|
25
|
+
outbox: current.outbox,
|
|
26
|
+
imports: current.imports
|
|
27
|
+
});
|
|
28
|
+
const replaceOutbox = Object.hasOwn(snapshot, 'outbox');
|
|
29
|
+
const replaceImports = Object.hasOwn(snapshot, 'imports');
|
|
30
|
+
const apply = () => this.replace(normalized, { replaceOutbox, replaceImports });
|
|
31
|
+
|
|
32
|
+
if (this.db.inTransaction) apply();
|
|
33
|
+
else this.db.transaction(apply)();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
replace(snapshot, { replaceOutbox, replaceImports }) {
|
|
37
|
+
const repositories = this.repositories;
|
|
38
|
+
repositories.subscriptions.deleteAll();
|
|
39
|
+
repositories.candidates.deleteAll();
|
|
40
|
+
repositories.facts.deleteAll();
|
|
41
|
+
repositories.episodes.deleteAll();
|
|
42
|
+
if (replaceOutbox) repositories.outbox.deleteAll();
|
|
43
|
+
if (replaceImports) repositories.imports.deleteAll();
|
|
44
|
+
repositories.spaces.deleteAll();
|
|
45
|
+
|
|
46
|
+
for (const space of snapshot.spaces) repositories.spaces.insertSnapshot(space);
|
|
47
|
+
for (const subscription of snapshot.subscriptions) {
|
|
48
|
+
repositories.subscriptions.insertSnapshot(subscription);
|
|
49
|
+
}
|
|
50
|
+
for (const episode of snapshot.episodes) repositories.episodes.insertSnapshot(episode);
|
|
51
|
+
for (const fact of snapshot.facts) {
|
|
52
|
+
repositories.facts.insertSnapshot(fact, { deferReplacement: true });
|
|
53
|
+
}
|
|
54
|
+
for (const fact of snapshot.facts) {
|
|
55
|
+
if (fact.replacedByFactId) {
|
|
56
|
+
repositories.facts.restoreReplacement(fact.id, fact.replacedByFactId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const candidate of snapshot.candidates) {
|
|
60
|
+
repositories.candidates.insertSnapshot(candidate);
|
|
61
|
+
}
|
|
62
|
+
if (replaceOutbox) {
|
|
63
|
+
for (const entry of snapshot.outbox) repositories.outbox.insertSnapshot(entry);
|
|
64
|
+
}
|
|
65
|
+
if (replaceImports) {
|
|
66
|
+
for (const record of snapshot.imports) repositories.imports.insertSnapshot(record);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|