fuli-context 0.4.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 +5 -0
- package/README.md +411 -173
- 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 -4
- 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 +60 -23
- package/src/cli/local-runtime-options.js +16 -6
- 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 +38 -3
- package/src/local-runtime/lifecycle.js +37 -16
- 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/lan-access.js +52 -0
- package/src/server.js +103 -25
- 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 +219 -58
- 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-CJCNI2Wd.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.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-BQRkOwPx.js +0 -2
- package/dist/web/assets/index-CpLDx3of.css +0 -1
- 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,113 +0,0 @@
|
|
|
1
|
-
CREATE TABLE schema_migrations_new (
|
|
2
|
-
version INTEGER PRIMARY KEY,
|
|
3
|
-
checksum TEXT NOT NULL,
|
|
4
|
-
applied_at TEXT NOT NULL
|
|
5
|
-
);
|
|
6
|
-
|
|
7
|
-
INSERT INTO schema_migrations_new (version, checksum, applied_at)
|
|
8
|
-
SELECT
|
|
9
|
-
version,
|
|
10
|
-
CASE version
|
|
11
|
-
WHEN 1 THEN 'ab393d736d8ff18fde75478002b2b4b3b4d7e906974cd724058e31436fba0ea9'
|
|
12
|
-
END,
|
|
13
|
-
applied_at
|
|
14
|
-
FROM schema_migrations;
|
|
15
|
-
|
|
16
|
-
DROP TABLE schema_migrations;
|
|
17
|
-
ALTER TABLE schema_migrations_new RENAME TO schema_migrations;
|
|
18
|
-
|
|
19
|
-
CREATE TABLE facts_new (
|
|
20
|
-
id TEXT PRIMARY KEY,
|
|
21
|
-
space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
22
|
-
subject TEXT NOT NULL,
|
|
23
|
-
predicate TEXT NOT NULL,
|
|
24
|
-
object TEXT NOT NULL,
|
|
25
|
-
source_episode_id TEXT NOT NULL REFERENCES episodes(id),
|
|
26
|
-
status TEXT NOT NULL CHECK (
|
|
27
|
-
status IN ('confirmed', 'observed', 'suggested', 'rejected', 'deprecated')
|
|
28
|
-
),
|
|
29
|
-
confidence REAL NOT NULL DEFAULT 1.0 CHECK (
|
|
30
|
-
typeof(confidence) IN ('integer', 'real') AND confidence >= 0.0 AND confidence <= 1.0
|
|
31
|
-
),
|
|
32
|
-
sensitivity TEXT NOT NULL DEFAULT 'normal' CHECK (
|
|
33
|
-
sensitivity IN ('normal', 'private', 'restricted')
|
|
34
|
-
),
|
|
35
|
-
scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('personal', 'public')),
|
|
36
|
-
valid_at TEXT NOT NULL,
|
|
37
|
-
invalid_at TEXT,
|
|
38
|
-
replaced_by_fact_id TEXT REFERENCES facts_new(id)
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
INSERT INTO facts_new (
|
|
42
|
-
id, space_id, subject, predicate, object, source_episode_id, status,
|
|
43
|
-
confidence, sensitivity, scope, valid_at, invalid_at, replaced_by_fact_id
|
|
44
|
-
)
|
|
45
|
-
SELECT
|
|
46
|
-
id, space_id, subject, predicate, object, source_episode_id, status,
|
|
47
|
-
confidence, sensitivity, scope, valid_at, invalid_at, NULL
|
|
48
|
-
FROM facts;
|
|
49
|
-
|
|
50
|
-
UPDATE facts_new
|
|
51
|
-
SET replaced_by_fact_id = (
|
|
52
|
-
SELECT facts.replaced_by_fact_id
|
|
53
|
-
FROM facts
|
|
54
|
-
WHERE facts.id = facts_new.id
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
DROP TABLE facts;
|
|
58
|
-
ALTER TABLE facts_new RENAME TO facts;
|
|
59
|
-
CREATE INDEX facts_current_idx ON facts(space_id, predicate, invalid_at);
|
|
60
|
-
CREATE INDEX facts_source_idx ON facts(source_episode_id);
|
|
61
|
-
|
|
62
|
-
CREATE TABLE candidates_new (
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
personal_space_id TEXT NOT NULL REFERENCES spaces(id),
|
|
65
|
-
target_space_id TEXT REFERENCES spaces(id),
|
|
66
|
-
episode_id TEXT NOT NULL REFERENCES episodes(id),
|
|
67
|
-
reason TEXT NOT NULL,
|
|
68
|
-
status TEXT NOT NULL CHECK (
|
|
69
|
-
status IN ('pending', 'synced', 'personal_only', 'ignored')
|
|
70
|
-
),
|
|
71
|
-
created_at TEXT NOT NULL,
|
|
72
|
-
decided_at TEXT
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
INSERT INTO candidates_new (
|
|
76
|
-
id, personal_space_id, target_space_id, episode_id, reason,
|
|
77
|
-
status, created_at, decided_at
|
|
78
|
-
)
|
|
79
|
-
SELECT
|
|
80
|
-
id, personal_space_id, target_space_id, episode_id, reason,
|
|
81
|
-
status, created_at, decided_at
|
|
82
|
-
FROM candidates;
|
|
83
|
-
|
|
84
|
-
DROP TABLE candidates;
|
|
85
|
-
ALTER TABLE candidates_new RENAME TO candidates;
|
|
86
|
-
|
|
87
|
-
CREATE TABLE outbox_new (
|
|
88
|
-
id TEXT PRIMARY KEY,
|
|
89
|
-
kind TEXT NOT NULL,
|
|
90
|
-
aggregate_id TEXT NOT NULL,
|
|
91
|
-
payload_json TEXT NOT NULL,
|
|
92
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent')),
|
|
93
|
-
attempts INTEGER NOT NULL DEFAULT 0 CHECK (
|
|
94
|
-
typeof(attempts) = 'integer' AND attempts >= 0
|
|
95
|
-
),
|
|
96
|
-
next_attempt_at TEXT,
|
|
97
|
-
created_at TEXT NOT NULL,
|
|
98
|
-
sent_at TEXT,
|
|
99
|
-
last_error TEXT
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
INSERT INTO outbox_new (
|
|
103
|
-
id, kind, aggregate_id, payload_json, status, attempts,
|
|
104
|
-
next_attempt_at, created_at, sent_at, last_error
|
|
105
|
-
)
|
|
106
|
-
SELECT
|
|
107
|
-
id, kind, aggregate_id, payload_json, status, attempts,
|
|
108
|
-
next_attempt_at, created_at, sent_at, last_error
|
|
109
|
-
FROM outbox;
|
|
110
|
-
|
|
111
|
-
DROP TABLE outbox;
|
|
112
|
-
ALTER TABLE outbox_new RENAME TO outbox;
|
|
113
|
-
CREATE INDEX outbox_pending_idx ON outbox(status, next_attempt_at, created_at);
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { CandidateStatus, FactScope, FactStatus, Sensitivity } from '../models.js';
|
|
2
|
-
|
|
3
|
-
const FACT_STATUSES = new Set(Object.values(FactStatus));
|
|
4
|
-
const CANDIDATE_STATUSES = new Set(Object.values(CandidateStatus));
|
|
5
|
-
const SENSITIVITIES = new Set(Object.values(Sensitivity));
|
|
6
|
-
const SCOPES = new Set(Object.values(FactScope));
|
|
7
|
-
const OUTBOX_STATUSES = new Set(['pending', 'sent']);
|
|
8
|
-
|
|
9
|
-
export function validateFactRecord(fact) {
|
|
10
|
-
requireMember('fact status', fact.status, FACT_STATUSES);
|
|
11
|
-
if (
|
|
12
|
-
typeof fact.confidence !== 'number' ||
|
|
13
|
-
!Number.isFinite(fact.confidence) ||
|
|
14
|
-
fact.confidence < 0 ||
|
|
15
|
-
fact.confidence > 1
|
|
16
|
-
) {
|
|
17
|
-
throw new TypeError('Fact confidence must be between 0 and 1');
|
|
18
|
-
}
|
|
19
|
-
requireMember('fact sensitivity', fact.sensitivity, SENSITIVITIES);
|
|
20
|
-
requireMember('fact scope', fact.scope, SCOPES);
|
|
21
|
-
return fact;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function validateCandidateRecord(candidate) {
|
|
25
|
-
requireMember('candidate status', candidate.status, CANDIDATE_STATUSES);
|
|
26
|
-
return candidate;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function validateOutboxRecord(entry) {
|
|
30
|
-
requireMember('outbox status', entry.status, OUTBOX_STATUSES);
|
|
31
|
-
if (!Number.isInteger(entry.attempts) || entry.attempts < 0) {
|
|
32
|
-
throw new TypeError('Outbox attempts must be a nonnegative integer');
|
|
33
|
-
}
|
|
34
|
-
return entry;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function validateSnapshotRecords(snapshot) {
|
|
38
|
-
for (const fact of snapshot.facts) validateFactRecord(fact);
|
|
39
|
-
for (const candidate of snapshot.candidates) validateCandidateRecord(candidate);
|
|
40
|
-
for (const entry of snapshot.outbox) validateOutboxRecord(entry);
|
|
41
|
-
return snapshot;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function requireMember(label, value, allowed) {
|
|
45
|
-
if (!allowed.has(value)) throw new TypeError(`Invalid ${label}: ${value}`);
|
|
46
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { CandidateStatus, FactStatus, nowIso } from '../models.js';
|
|
2
|
-
import { validateSnapshotRecords } from './record-validation.js';
|
|
3
|
-
|
|
4
|
-
export function normalizeSnapshot(snapshot = {}, { outbox = [], imports = [] } = {}) {
|
|
5
|
-
const input = cloneSnapshot(snapshot);
|
|
6
|
-
const normalized = {
|
|
7
|
-
spaces: (input.spaces ?? []).map((space) => ({
|
|
8
|
-
...space,
|
|
9
|
-
description: space.description ?? null,
|
|
10
|
-
createdAt: space.createdAt ?? nowIso()
|
|
11
|
-
})),
|
|
12
|
-
subscriptions: (input.subscriptions ?? []).map((subscription) => ({
|
|
13
|
-
...subscription,
|
|
14
|
-
mode: subscription.mode ?? 'latest',
|
|
15
|
-
createdAt: subscription.createdAt ?? nowIso()
|
|
16
|
-
})),
|
|
17
|
-
episodes: (input.episodes ?? []).map((episode) => ({
|
|
18
|
-
...episode,
|
|
19
|
-
sourceUri: episode.sourceUri ?? null,
|
|
20
|
-
metadata: episode.metadata ?? {},
|
|
21
|
-
createdAt: episode.createdAt ?? nowIso()
|
|
22
|
-
})),
|
|
23
|
-
facts: (input.facts ?? []).map((fact) => ({
|
|
24
|
-
...fact,
|
|
25
|
-
status: fact.status ?? FactStatus.CONFIRMED,
|
|
26
|
-
confidence: fact.confidence ?? 1,
|
|
27
|
-
sensitivity: fact.sensitivity ?? 'normal',
|
|
28
|
-
scope: fact.scope ?? 'personal',
|
|
29
|
-
validAt: fact.validAt ?? nowIso(),
|
|
30
|
-
invalidAt: fact.invalidAt ?? null,
|
|
31
|
-
replacedByFactId: fact.replacedByFactId ?? null
|
|
32
|
-
})),
|
|
33
|
-
candidates: (input.candidates ?? []).map(normalizeCandidate),
|
|
34
|
-
outbox: Object.hasOwn(input, 'outbox') ? input.outbox : cloneSnapshot(outbox),
|
|
35
|
-
imports: Object.hasOwn(input, 'imports') ? input.imports : cloneSnapshot(imports)
|
|
36
|
-
};
|
|
37
|
-
return validateSnapshotRecords(normalized);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function cloneSnapshot(value) {
|
|
41
|
-
return JSON.parse(JSON.stringify(value));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeCandidate(candidate) {
|
|
45
|
-
const status = candidate.status ?? CandidateStatus.PENDING;
|
|
46
|
-
const createdAt = candidate.createdAt ?? nowIso();
|
|
47
|
-
return {
|
|
48
|
-
...candidate,
|
|
49
|
-
targetSpaceId: candidate.targetSpaceId ?? null,
|
|
50
|
-
status,
|
|
51
|
-
createdAt,
|
|
52
|
-
decidedAt: candidate.decidedAt ?? (status === CandidateStatus.PENDING ? null : createdAt)
|
|
53
|
-
};
|
|
54
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { CandidateStatus, nowIso } from '../../models.js';
|
|
4
|
-
import { validateCandidateRecord } from '../record-validation.js';
|
|
5
|
-
import { mapCandidate } from './mapper.js';
|
|
6
|
-
|
|
7
|
-
export class CandidatesRepository {
|
|
8
|
-
constructor(db) {
|
|
9
|
-
this.insertStatement = db.prepare(`
|
|
10
|
-
INSERT INTO candidates (
|
|
11
|
-
id, personal_space_id, target_space_id, episode_id, reason,
|
|
12
|
-
status, created_at, decided_at
|
|
13
|
-
) VALUES (
|
|
14
|
-
@id, @personalSpaceId, @targetSpaceId, @episodeId, @reason,
|
|
15
|
-
@status, @createdAt, @decidedAt
|
|
16
|
-
)
|
|
17
|
-
`);
|
|
18
|
-
this.getStatement = db.prepare('SELECT * FROM candidates WHERE id = ?');
|
|
19
|
-
this.listStatement = db.prepare('SELECT * FROM candidates ORDER BY rowid');
|
|
20
|
-
this.pendingStatement = db.prepare(`
|
|
21
|
-
SELECT * FROM candidates
|
|
22
|
-
WHERE personal_space_id = ? AND status = 'pending'
|
|
23
|
-
ORDER BY rowid
|
|
24
|
-
`);
|
|
25
|
-
this.updateStatusStatement = db.prepare(`
|
|
26
|
-
UPDATE candidates SET status = ?, decided_at = ? WHERE id = ?
|
|
27
|
-
`);
|
|
28
|
-
this.deleteStatement = db.prepare('DELETE FROM candidates');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
add(candidate) {
|
|
32
|
-
if (candidate.id && this.get(candidate.id)) {
|
|
33
|
-
throw new Error(`Duplicate candidate id: ${candidate.id}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const status = candidate.status ?? CandidateStatus.PENDING;
|
|
37
|
-
const createdAt = candidate.createdAt ?? nowIso();
|
|
38
|
-
const stored = {
|
|
39
|
-
id: candidate.id || randomUUID(),
|
|
40
|
-
personalSpaceId: candidate.personalSpaceId,
|
|
41
|
-
targetSpaceId: candidate.targetSpaceId ?? null,
|
|
42
|
-
episodeId: candidate.episodeId,
|
|
43
|
-
reason: candidate.reason,
|
|
44
|
-
status,
|
|
45
|
-
createdAt,
|
|
46
|
-
decidedAt: candidate.decidedAt ?? (
|
|
47
|
-
status === CandidateStatus.PENDING ? null : createdAt
|
|
48
|
-
)
|
|
49
|
-
};
|
|
50
|
-
validateCandidateRecord(stored);
|
|
51
|
-
this.insertStatement.run(stored);
|
|
52
|
-
return this.get(stored.id);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
get(id) {
|
|
56
|
-
return mapCandidate(this.getStatement.get(id));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
list() {
|
|
60
|
-
return this.listStatement.all().map(mapCandidate);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
pending(personalSpaceId) {
|
|
64
|
-
return this.pendingStatement.all(personalSpaceId).map(mapCandidate);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
updateStatus(id, status) {
|
|
68
|
-
const candidate = this.get(id);
|
|
69
|
-
if (!candidate) throw new Error(`Candidate not found: ${id}`);
|
|
70
|
-
const decidedAt = status === CandidateStatus.PENDING ? null : nowIso();
|
|
71
|
-
validateCandidateRecord({ ...candidate, status, decidedAt });
|
|
72
|
-
this.updateStatusStatement.run(status, decidedAt, id);
|
|
73
|
-
return this.get(id);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
insertSnapshot(candidate) {
|
|
77
|
-
validateCandidateRecord(candidate);
|
|
78
|
-
this.insertStatement.run(candidate);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
deleteAll() {
|
|
82
|
-
this.deleteStatement.run();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_EPISODE_METADATA_BYTES,
|
|
3
|
-
MAX_EPISODE_METADATA_FIELD_BYTES,
|
|
4
|
-
normalizeCorrectionEvidencePreviewOptions,
|
|
5
|
-
normalizeEvidencePreviewOptions,
|
|
6
|
-
previewEpisode
|
|
7
|
-
} from '../episode-evidence-preview.js';
|
|
8
|
-
import { sqliteEpisodeRestrictedSql } from './episode-privacy-sql.js';
|
|
9
|
-
|
|
10
|
-
export class EpisodeEvidenceRepository {
|
|
11
|
-
constructor(db) {
|
|
12
|
-
this.db = db;
|
|
13
|
-
this.correctionStatements = new Map();
|
|
14
|
-
this.sourceStatement = db.prepare(sourceQuery());
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
source(spaceId, episodeId, options = {}) {
|
|
18
|
-
const normalized = normalizeEvidencePreviewOptions(options);
|
|
19
|
-
const row = this.sourceStatement.get({
|
|
20
|
-
spaceId, episodeId,
|
|
21
|
-
bodyChars: normalized.maxBodyBytes,
|
|
22
|
-
bodyBytes: normalized.maxBodyBytes,
|
|
23
|
-
metadataChars: MAX_EPISODE_METADATA_BYTES,
|
|
24
|
-
metadataBytes: MAX_EPISODE_METADATA_BYTES,
|
|
25
|
-
includeRestricted: Number(normalized.includeRestricted)
|
|
26
|
-
});
|
|
27
|
-
return previewEpisode(mapSourceRow(row), normalized);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
corrections(spaceId, factIds, options = {}) {
|
|
31
|
-
const normalized = normalizeCorrectionEvidencePreviewOptions(options);
|
|
32
|
-
if (factIds.length === 0) return [];
|
|
33
|
-
const statement = this.#correctionStatement(factIds.length);
|
|
34
|
-
const parameters = {
|
|
35
|
-
spaceId,
|
|
36
|
-
bodyChars: normalized.maxBodyBytes,
|
|
37
|
-
bodyBytes: normalized.maxBodyBytes,
|
|
38
|
-
fieldChars: MAX_EPISODE_METADATA_FIELD_BYTES,
|
|
39
|
-
fieldBytes: MAX_EPISODE_METADATA_FIELD_BYTES,
|
|
40
|
-
correctionLimit: normalized.maxCorrectionsPerFact,
|
|
41
|
-
includeRestricted: Number(normalized.includeRestricted)
|
|
42
|
-
};
|
|
43
|
-
factIds.forEach((factId, index) => { parameters[`factId${index}`] = factId; });
|
|
44
|
-
const groups = [...new Set(factIds)].map((factId) => ({
|
|
45
|
-
factId,
|
|
46
|
-
episodes: [],
|
|
47
|
-
truncated: false
|
|
48
|
-
}));
|
|
49
|
-
const byFactId = new Map(groups.map((group) => [group.factId, group]));
|
|
50
|
-
for (const row of statement.all(parameters)) {
|
|
51
|
-
const group = byFactId.get(row.fact_id);
|
|
52
|
-
group.episodes.push(previewEpisode(
|
|
53
|
-
mapCorrectionRow(row),
|
|
54
|
-
normalized,
|
|
55
|
-
{ correction: true }
|
|
56
|
-
));
|
|
57
|
-
group.truncated = row.correction_count > normalized.maxCorrectionsPerFact;
|
|
58
|
-
}
|
|
59
|
-
return groups;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
#correctionStatement(factCount) {
|
|
63
|
-
if (!this.correctionStatements.has(factCount)) {
|
|
64
|
-
this.correctionStatements.set(factCount, this.db.prepare(correctionQuery(factCount)));
|
|
65
|
-
}
|
|
66
|
-
return this.correctionStatements.get(factCount);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function sourceQuery() {
|
|
71
|
-
return `
|
|
72
|
-
SELECT id, space_id, source_kind,
|
|
73
|
-
substr(body, 1, @bodyChars) AS body_preview,
|
|
74
|
-
length(CAST(body AS BLOB)) > @bodyBytes AS body_truncated,
|
|
75
|
-
source_uri,
|
|
76
|
-
substr(metadata_json, 1, @metadataChars) AS metadata_preview,
|
|
77
|
-
length(CAST(metadata_json AS BLOB)) > @metadataBytes AS metadata_truncated,
|
|
78
|
-
created_at
|
|
79
|
-
FROM episodes source
|
|
80
|
-
WHERE id = @episodeId AND space_id = @spaceId
|
|
81
|
-
AND (@includeRestricted = 1 OR NOT ${sqliteEpisodeRestrictedSql('source')})
|
|
82
|
-
`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function correctionQuery(factCount) {
|
|
86
|
-
const factIds = Array.from({ length: factCount }, (_, index) => `@factId${index}`).join(', ');
|
|
87
|
-
const fields = ['kind', 'factId', 'action'];
|
|
88
|
-
const selected = fields.map((field) => `
|
|
89
|
-
substr(CAST(json_extract(metadata_json, '$.${field}') AS TEXT), 1, @fieldChars)
|
|
90
|
-
AS metadata_${field.toLowerCase()}`
|
|
91
|
-
).join(',');
|
|
92
|
-
const clipped = fields.map((field) => `
|
|
93
|
-
length(CAST(json_extract(metadata_json, '$.${field}') AS BLOB)) > @fieldBytes`
|
|
94
|
-
).join(' OR ');
|
|
95
|
-
return `
|
|
96
|
-
WITH ranked AS (
|
|
97
|
-
SELECT correction.rowid AS correction_rowid,
|
|
98
|
-
json_extract(metadata_json, '$.factId') AS fact_id,
|
|
99
|
-
row_number() OVER (
|
|
100
|
-
PARTITION BY json_extract(metadata_json, '$.factId')
|
|
101
|
-
ORDER BY created_at DESC, correction.rowid DESC
|
|
102
|
-
) AS correction_rank,
|
|
103
|
-
count(*) OVER (
|
|
104
|
-
PARTITION BY json_extract(metadata_json, '$.factId')
|
|
105
|
-
) AS correction_count
|
|
106
|
-
FROM episodes correction
|
|
107
|
-
WHERE space_id = @spaceId
|
|
108
|
-
AND json_extract(metadata_json, '$.kind') = 'lens_correction'
|
|
109
|
-
AND json_extract(metadata_json, '$.factId') IN (${factIds})
|
|
110
|
-
AND (@includeRestricted = 1 OR NOT ${sqliteEpisodeRestrictedSql('correction')})
|
|
111
|
-
)
|
|
112
|
-
SELECT correction.id, correction.space_id, correction.source_kind,
|
|
113
|
-
substr(body, 1, @bodyChars) AS body_preview,
|
|
114
|
-
length(CAST(body AS BLOB)) > @bodyBytes AS body_truncated,
|
|
115
|
-
source_uri,${selected}, ranked.fact_id, ranked.correction_count,
|
|
116
|
-
((${clipped}) OR EXISTS (
|
|
117
|
-
SELECT 1 FROM json_each(metadata_json)
|
|
118
|
-
WHERE key NOT IN ('kind', 'factId', 'action')
|
|
119
|
-
)) AS metadata_truncated,
|
|
120
|
-
correction.created_at
|
|
121
|
-
FROM ranked
|
|
122
|
-
JOIN episodes correction ON correction.rowid = ranked.correction_rowid
|
|
123
|
-
WHERE ranked.correction_rank <= @correctionLimit
|
|
124
|
-
ORDER BY ranked.fact_id, ranked.correction_rank
|
|
125
|
-
`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function mapSourceRow(row) {
|
|
129
|
-
if (!row) return null;
|
|
130
|
-
return mapRow(row, row.metadata_truncated ? {} : JSON.parse(row.metadata_preview));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function mapCorrectionRow(row) {
|
|
134
|
-
if (!row) return null;
|
|
135
|
-
const metadata = {};
|
|
136
|
-
for (const [key, column] of [
|
|
137
|
-
['kind', 'metadata_kind'], ['factId', 'metadata_factid'], ['action', 'metadata_action']
|
|
138
|
-
]) if (typeof row[column] === 'string') metadata[key] = row[column];
|
|
139
|
-
return mapRow(row, metadata);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function mapRow(row, metadata) {
|
|
143
|
-
return {
|
|
144
|
-
id: row.id,
|
|
145
|
-
spaceId: row.space_id,
|
|
146
|
-
sourceKind: row.source_kind,
|
|
147
|
-
body: row.body_preview,
|
|
148
|
-
bodyTruncated: row.body_truncated === 1,
|
|
149
|
-
sourceUri: row.source_uri,
|
|
150
|
-
metadata,
|
|
151
|
-
metadataTruncated: row.metadata_truncated === 1,
|
|
152
|
-
createdAt: row.created_at
|
|
153
|
-
};
|
|
154
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { isRestrictedEpisode } from '../../security/episode-visibility.js';
|
|
2
|
-
|
|
3
|
-
const FUNCTION_NAME = 'episode_evidence_is_restricted';
|
|
4
|
-
|
|
5
|
-
export function registerEpisodePrivacyFunction(db) {
|
|
6
|
-
db.function(
|
|
7
|
-
FUNCTION_NAME,
|
|
8
|
-
{ deterministic: true },
|
|
9
|
-
(sourceKind, body, sourceUri, metadataJson) => Number(isRestrictedEpisode({
|
|
10
|
-
sourceKind,
|
|
11
|
-
body,
|
|
12
|
-
sourceUri,
|
|
13
|
-
metadata: parseMetadata(metadataJson)
|
|
14
|
-
}))
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function sqliteEpisodeRestrictedSql(alias) {
|
|
19
|
-
return `${FUNCTION_NAME}(
|
|
20
|
-
${alias}.source_kind,
|
|
21
|
-
${alias}.body,
|
|
22
|
-
${alias}.source_uri,
|
|
23
|
-
${alias}.metadata_json
|
|
24
|
-
) = 1`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function parseMetadata(value) {
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(value);
|
|
30
|
-
} catch {
|
|
31
|
-
return { invalidMetadataRestricted: 'password=invalid-metadata' };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { nowIso } from '../../models.js';
|
|
4
|
-
import { mapEpisode } from './mapper.js';
|
|
5
|
-
|
|
6
|
-
export class EpisodesRepository {
|
|
7
|
-
constructor(db) {
|
|
8
|
-
this.insertStatement = db.prepare(`
|
|
9
|
-
INSERT INTO episodes (
|
|
10
|
-
id, space_id, source_kind, body, source_uri, metadata_json, created_at
|
|
11
|
-
) VALUES (
|
|
12
|
-
@id, @spaceId, @sourceKind, @body, @sourceUri, @metadataJson, @createdAt
|
|
13
|
-
)
|
|
14
|
-
`);
|
|
15
|
-
this.getStatement = db.prepare('SELECT * FROM episodes WHERE id = ?');
|
|
16
|
-
this.listStatement = db.prepare('SELECT * FROM episodes ORDER BY rowid');
|
|
17
|
-
this.deleteStatement = db.prepare('DELETE FROM episodes');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
add(spaceId, sourceKind, body, sourceUri = null, metadata = {}) {
|
|
21
|
-
const episode = {
|
|
22
|
-
id: randomUUID(),
|
|
23
|
-
spaceId,
|
|
24
|
-
sourceKind,
|
|
25
|
-
body,
|
|
26
|
-
sourceUri,
|
|
27
|
-
metadataJson: JSON.stringify(metadata),
|
|
28
|
-
createdAt: nowIso()
|
|
29
|
-
};
|
|
30
|
-
this.insertStatement.run(episode);
|
|
31
|
-
return this.get(episode.id);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
get(id) {
|
|
35
|
-
return mapEpisode(this.getStatement.get(id));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
list() {
|
|
39
|
-
return this.listStatement.all().map(mapEpisode);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
insertSnapshot(episode) {
|
|
43
|
-
this.insertStatement.run({
|
|
44
|
-
...episode,
|
|
45
|
-
metadataJson: JSON.stringify(episode.metadata)
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
deleteAll() {
|
|
50
|
-
this.deleteStatement.run();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { isRestrictedFact } from '../../security/fact-visibility.js';
|
|
2
|
-
import { searchTextSortKey } from '../fact-search-order.js';
|
|
3
|
-
import { sqliteEpisodeRestrictedSql } from './episode-privacy-sql.js';
|
|
4
|
-
import { mapFact } from './mapper.js';
|
|
5
|
-
|
|
6
|
-
export class FactSearchRepository {
|
|
7
|
-
constructor(db) {
|
|
8
|
-
this.db = db;
|
|
9
|
-
this.statements = new Map();
|
|
10
|
-
this.omissionStatements = new Map();
|
|
11
|
-
registerSearchFunctions(db);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
search(spaceIds, query, {
|
|
15
|
-
includeHistorical = false,
|
|
16
|
-
scope = null,
|
|
17
|
-
limit = null,
|
|
18
|
-
excludeRestricted = false,
|
|
19
|
-
safeSourceSpaceId = null,
|
|
20
|
-
maxObjectBytes = null
|
|
21
|
-
} = {}) {
|
|
22
|
-
return this.searchPage(spaceIds, query, {
|
|
23
|
-
includeHistorical,
|
|
24
|
-
scope,
|
|
25
|
-
limit,
|
|
26
|
-
excludeRestricted,
|
|
27
|
-
safeSourceSpaceId,
|
|
28
|
-
maxObjectBytes
|
|
29
|
-
}).facts;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
searchPage(spaceIds, query, options = {}) {
|
|
33
|
-
const {
|
|
34
|
-
includeHistorical = false,
|
|
35
|
-
scope = null,
|
|
36
|
-
limit = null,
|
|
37
|
-
excludeRestricted = false,
|
|
38
|
-
safeSourceSpaceId = null,
|
|
39
|
-
maxObjectBytes = null
|
|
40
|
-
} = options;
|
|
41
|
-
if (spaceIds.length === 0) return { facts: [], objectLimitTruncated: false };
|
|
42
|
-
if (limit !== null && (!Number.isInteger(limit) || limit <= 0)) {
|
|
43
|
-
throw new TypeError('Fact search limit must be a positive integer');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const shape = {
|
|
47
|
-
spaceCount: spaceIds.length,
|
|
48
|
-
includeHistorical,
|
|
49
|
-
scoped: scope !== null,
|
|
50
|
-
limited: limit !== null,
|
|
51
|
-
excludeRestricted: excludeRestricted === true,
|
|
52
|
-
safeSourceRequired: safeSourceSpaceId !== null,
|
|
53
|
-
objectBytesBounded: maxObjectBytes !== null
|
|
54
|
-
};
|
|
55
|
-
const parameters = [...spaceIds];
|
|
56
|
-
if (scope !== null) parameters.push(scope);
|
|
57
|
-
if (safeSourceSpaceId !== null) parameters.push(safeSourceSpaceId);
|
|
58
|
-
if (maxObjectBytes !== null) parameters.push(maxObjectBytes);
|
|
59
|
-
parameters.push(query.toLowerCase());
|
|
60
|
-
const factParameters = limit === null ? parameters : [...parameters, limit];
|
|
61
|
-
const facts = this.#statement(shape).all(...factParameters).map(mapFact);
|
|
62
|
-
const objectLimitTruncated = maxObjectBytes === null
|
|
63
|
-
? false
|
|
64
|
-
: this.#omissionStatement(shape).pluck().get(...parameters) === 1;
|
|
65
|
-
return { facts, objectLimitTruncated };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
#statement(shape) {
|
|
69
|
-
const key = Object.values(shape).join(':');
|
|
70
|
-
if (!this.statements.has(key)) {
|
|
71
|
-
this.statements.set(key, this.db.prepare(buildSearchQuery(shape)));
|
|
72
|
-
}
|
|
73
|
-
return this.statements.get(key);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
#omissionStatement(shape) {
|
|
77
|
-
const key = Object.values(shape).join(':');
|
|
78
|
-
if (!this.omissionStatements.has(key)) {
|
|
79
|
-
this.omissionStatements.set(key, this.db.prepare(buildOmissionQuery(shape)));
|
|
80
|
-
}
|
|
81
|
-
return this.omissionStatements.get(key);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function buildOmissionQuery(shape) {
|
|
86
|
-
return `SELECT EXISTS (
|
|
87
|
-
SELECT 1 FROM facts
|
|
88
|
-
WHERE ${buildWhereClause(shape, { oversized: true })}
|
|
89
|
-
LIMIT 1
|
|
90
|
-
)`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function registerSearchFunctions(db) {
|
|
94
|
-
db.function('fact_search_text_sort_key', { deterministic: true }, searchTextSortKey);
|
|
95
|
-
db.function('fact_search_is_restricted', { deterministic: true },
|
|
96
|
-
(subject, predicate, object, sensitivity) => Number(isRestrictedFact({
|
|
97
|
-
subject, predicate, object, sensitivity
|
|
98
|
-
})));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function buildSearchQuery({
|
|
102
|
-
spaceCount,
|
|
103
|
-
includeHistorical,
|
|
104
|
-
scoped,
|
|
105
|
-
limited,
|
|
106
|
-
excludeRestricted,
|
|
107
|
-
safeSourceRequired,
|
|
108
|
-
objectBytesBounded
|
|
109
|
-
}) {
|
|
110
|
-
const shape = {
|
|
111
|
-
spaceCount,
|
|
112
|
-
includeHistorical,
|
|
113
|
-
scoped,
|
|
114
|
-
excludeRestricted,
|
|
115
|
-
safeSourceRequired,
|
|
116
|
-
objectBytesBounded
|
|
117
|
-
};
|
|
118
|
-
const orderClause = limited
|
|
119
|
-
? 'ORDER BY valid_at DESC, fact_search_text_sort_key(id) ASC LIMIT ?'
|
|
120
|
-
: 'ORDER BY rowid';
|
|
121
|
-
return `
|
|
122
|
-
SELECT * FROM facts
|
|
123
|
-
WHERE ${buildWhereClause(shape)}
|
|
124
|
-
${orderClause}
|
|
125
|
-
`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildWhereClause({
|
|
129
|
-
spaceCount,
|
|
130
|
-
includeHistorical,
|
|
131
|
-
scoped,
|
|
132
|
-
excludeRestricted,
|
|
133
|
-
safeSourceRequired,
|
|
134
|
-
objectBytesBounded
|
|
135
|
-
}, { oversized = false } = {}) {
|
|
136
|
-
const placeholders = Array.from({ length: spaceCount }, () => '?').join(', ');
|
|
137
|
-
const currentClause = includeHistorical
|
|
138
|
-
? ''
|
|
139
|
-
: "AND invalid_at IS NULL AND status NOT IN ('rejected', 'deprecated')";
|
|
140
|
-
const scopeClause = scoped ? 'AND scope = ?' : '';
|
|
141
|
-
const restrictedClause = excludeRestricted
|
|
142
|
-
? `AND fact_search_is_restricted(
|
|
143
|
-
subject, predicate, object, sensitivity
|
|
144
|
-
) = 0`
|
|
145
|
-
: '';
|
|
146
|
-
const sourceClause = safeSourceRequired ? `AND EXISTS (
|
|
147
|
-
SELECT 1 FROM episodes source
|
|
148
|
-
WHERE source.id = source_episode_id
|
|
149
|
-
AND source.space_id = ?
|
|
150
|
-
AND NOT ${sqliteEpisodeRestrictedSql('source')}
|
|
151
|
-
)` : '';
|
|
152
|
-
const objectBytesClause = objectBytesBounded
|
|
153
|
-
? `AND length(CAST(object AS BLOB)) ${oversized ? '>' : '<='} ?`
|
|
154
|
-
: '';
|
|
155
|
-
return `space_id IN (${placeholders})
|
|
156
|
-
${currentClause}
|
|
157
|
-
${scopeClause}
|
|
158
|
-
${restrictedClause}
|
|
159
|
-
${sourceClause}
|
|
160
|
-
${objectBytesClause}
|
|
161
|
-
AND instr(lower(subject || ' ' || predicate || ' ' || object), ?) > 0`;
|
|
162
|
-
}
|