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,37 +0,0 @@
|
|
|
1
|
-
import { contentHash } from './canonical-json.js';
|
|
2
|
-
import { validateEnvelopeSchema } from './publication-envelope-schema.js';
|
|
3
|
-
|
|
4
|
-
export function createEnvelope({ id, spaceId, episode, facts }) {
|
|
5
|
-
const unsigned = {
|
|
6
|
-
id,
|
|
7
|
-
spaceId,
|
|
8
|
-
source: {
|
|
9
|
-
episodeId: episode.id,
|
|
10
|
-
kind: episode.sourceKind,
|
|
11
|
-
uri: episode.sourceUri,
|
|
12
|
-
capturedAt: episode.createdAt
|
|
13
|
-
},
|
|
14
|
-
facts: facts.map(projectFact),
|
|
15
|
-
policyVersion: '1'
|
|
16
|
-
};
|
|
17
|
-
return validateEnvelopeSchema({ ...unsigned, contentHash: contentHash(unsigned) });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function verifyEnvelope(envelope) {
|
|
21
|
-
validateEnvelopeSchema(envelope);
|
|
22
|
-
const { contentHash: suppliedHash, ...unsigned } = envelope;
|
|
23
|
-
if (contentHash(unsigned) !== suppliedHash) {
|
|
24
|
-
throw new Error('Publication envelope content hash does not match');
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function projectFact(fact) {
|
|
30
|
-
return {
|
|
31
|
-
id: fact.id,
|
|
32
|
-
subject: fact.subject,
|
|
33
|
-
predicate: fact.predicate,
|
|
34
|
-
object: fact.object,
|
|
35
|
-
sourceEpisodeId: fact.sourceEpisodeId
|
|
36
|
-
};
|
|
37
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FactScope,
|
|
3
|
-
FactStatus,
|
|
4
|
-
Sensitivity,
|
|
5
|
-
SpaceKind,
|
|
6
|
-
isCurrentFact
|
|
7
|
-
} from '../models.js';
|
|
8
|
-
import { detectSensitiveContent } from '../security/sensitive-content.js';
|
|
9
|
-
import { canonicalJson } from './canonical-json.js';
|
|
10
|
-
|
|
11
|
-
export function validatePublication(store, { spaceId, episode, facts }) {
|
|
12
|
-
const space = store.getSpace(spaceId);
|
|
13
|
-
if (!space || space.kind !== SpaceKind.PUBLIC) {
|
|
14
|
-
throw new Error(`Publication target must be a public space: ${spaceId}`);
|
|
15
|
-
}
|
|
16
|
-
if (!episode?.id) throw new Error('Publication source episode is required');
|
|
17
|
-
|
|
18
|
-
const storedEpisode = store.getEpisode(episode.id);
|
|
19
|
-
if (!storedEpisode) throw new Error(`Publication source episode not found: ${episode.id}`);
|
|
20
|
-
if (storedEpisode.spaceId !== spaceId) throw new Error('Publication episode crosses spaces');
|
|
21
|
-
assertAuthentic('episode', episode, storedEpisode);
|
|
22
|
-
assertNoSensitiveSource(storedEpisode);
|
|
23
|
-
|
|
24
|
-
if (!Array.isArray(facts) || facts.length === 0) {
|
|
25
|
-
throw new TypeError('Publication facts must be a non-empty array');
|
|
26
|
-
}
|
|
27
|
-
const factIds = facts.map((fact) => fact?.id);
|
|
28
|
-
if (new Set(factIds).size !== factIds.length) {
|
|
29
|
-
throw new TypeError('Publication facts contain a duplicate fact id');
|
|
30
|
-
}
|
|
31
|
-
const storedFacts = facts.map((fact) => validateFact(store, fact, spaceId, storedEpisode.id));
|
|
32
|
-
return { episode: storedEpisode, facts: storedFacts };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function validateFact(store, fact, spaceId, episodeId) {
|
|
36
|
-
if (!fact?.id) throw new Error('Publication fact is required');
|
|
37
|
-
const stored = store.getFact(fact.id);
|
|
38
|
-
if (!stored) throw new Error(`Publication fact not found: ${fact.id}`);
|
|
39
|
-
assertAuthentic('fact', fact, stored);
|
|
40
|
-
if (stored.spaceId !== spaceId) throw new Error('Publication fact crosses spaces');
|
|
41
|
-
if (stored.sourceEpisodeId !== episodeId) throw new Error('Publication fact has missing source');
|
|
42
|
-
if (stored.scope !== FactScope.PUBLIC) throw new Error('Personal facts cannot be published');
|
|
43
|
-
if (stored.status !== FactStatus.CONFIRMED) throw new Error('Publication fact is not confirmed');
|
|
44
|
-
if (stored.sensitivity !== Sensitivity.NORMAL) {
|
|
45
|
-
throw new Error('Private or restricted facts cannot be published');
|
|
46
|
-
}
|
|
47
|
-
if (!isCurrentFact(stored) || !store.currentFacts(spaceId).some(({ id }) => id === stored.id)) {
|
|
48
|
-
throw new Error('Historical facts cannot be published');
|
|
49
|
-
}
|
|
50
|
-
assertSafeTexts([stored.subject, stored.predicate, stored.object]);
|
|
51
|
-
return stored;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function assertAuthentic(label, supplied, stored) {
|
|
55
|
-
if (canonicalJson(supplied) !== canonicalJson(stored)) {
|
|
56
|
-
throw new Error(`Publication ${label} does not match stored record`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function assertNoSensitiveSource(episode) {
|
|
61
|
-
assertSafeTexts([
|
|
62
|
-
episode.body,
|
|
63
|
-
episode.sourceKind,
|
|
64
|
-
episode.sourceUri,
|
|
65
|
-
...metadataStrings(episode.metadata)
|
|
66
|
-
]);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function assertSafeTexts(texts) {
|
|
70
|
-
if (texts.some((text) => detectSensitiveContent(text).restricted)) {
|
|
71
|
-
throw new Error('Detected secret cannot be published');
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function metadataStrings(value) {
|
|
76
|
-
if (typeof value === 'string') return [value];
|
|
77
|
-
if (Array.isArray(value)) return value.flatMap(metadataStrings);
|
|
78
|
-
if (value && typeof value === 'object') {
|
|
79
|
-
return Object.entries(value).flatMap(([key, item]) => [key, ...metadataStrings(item)]);
|
|
80
|
-
}
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { createEnvelope, verifyEnvelope } from './publication-envelope.js';
|
|
4
|
-
import { validatePublication } from './publication-policy.js';
|
|
5
|
-
|
|
6
|
-
export class PublicationService {
|
|
7
|
-
constructor(store, { id = randomUUID } = {}) {
|
|
8
|
-
this.store = store;
|
|
9
|
-
this.id = id;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
prepare(input) {
|
|
13
|
-
validatePublication(this.store, input);
|
|
14
|
-
return this.store.transaction(
|
|
15
|
-
() => this.#validateBuildAndEnqueue(input),
|
|
16
|
-
{ mode: 'immediate' }
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
prepareInCurrentTransaction(input) {
|
|
21
|
-
return this.#validateBuildAndEnqueue(input);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
verify(envelope) {
|
|
25
|
-
return verifyEnvelope(envelope);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
#validateBuildAndEnqueue(input) {
|
|
29
|
-
const records = validatePublication(this.store, input);
|
|
30
|
-
return this.#enqueue({
|
|
31
|
-
spaceId: input.spaceId,
|
|
32
|
-
envelope: createEnvelope({
|
|
33
|
-
id: this.id(),
|
|
34
|
-
spaceId: input.spaceId,
|
|
35
|
-
...records
|
|
36
|
-
})
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
#enqueue({ spaceId, envelope }) {
|
|
41
|
-
const outbox = this.store.enqueueOutbox({
|
|
42
|
-
kind: 'publication',
|
|
43
|
-
aggregateId: `${spaceId}:${envelope.id}`,
|
|
44
|
-
payload: { envelope }
|
|
45
|
-
});
|
|
46
|
-
return { envelope, outbox };
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/router.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { isCurrentFact } from './models.js';
|
|
2
|
-
|
|
3
|
-
export class ContextRouter {
|
|
4
|
-
constructor(store) {
|
|
5
|
-
this.store = store;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
searchContext({ personalSpaceId, query, includeHistorical = false }) {
|
|
9
|
-
const spaceIds = [
|
|
10
|
-
personalSpaceId,
|
|
11
|
-
...this.store.subscriptionsFor(personalSpaceId).map((subscription) => subscription.spaceId)
|
|
12
|
-
];
|
|
13
|
-
const facts = this.store.searchFacts(spaceIds, query, { includeHistorical });
|
|
14
|
-
const matches = facts.map((fact) => buildMatch(this.store, fact));
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
query,
|
|
18
|
-
spaceIds,
|
|
19
|
-
matches,
|
|
20
|
-
facts,
|
|
21
|
-
answer: formatAnswer(facts)
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildMatch(store, fact) {
|
|
27
|
-
const episode = store.getEpisode(fact.sourceEpisodeId);
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
spaceId: fact.spaceId,
|
|
31
|
-
spaceName: store.getSpace(fact.spaceId)?.name ?? fact.spaceId,
|
|
32
|
-
current: isCurrentFact(fact),
|
|
33
|
-
fact,
|
|
34
|
-
source: episode
|
|
35
|
-
? {
|
|
36
|
-
id: episode.id,
|
|
37
|
-
kind: episode.sourceKind,
|
|
38
|
-
uri: episode.sourceUri,
|
|
39
|
-
body: episode.body,
|
|
40
|
-
createdAt: episode.createdAt
|
|
41
|
-
}
|
|
42
|
-
: null
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function formatAnswer(facts) {
|
|
47
|
-
if (facts.length === 0) {
|
|
48
|
-
return '没有找到相关当前事实';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return facts
|
|
52
|
-
.map((fact) => `${fact.subject}:${humanPredicate(fact.predicate)} 是 ${fact.object}`)
|
|
53
|
-
.join('\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function humanPredicate(predicate) {
|
|
57
|
-
return predicate.startsWith('has_') ? predicate.slice(4) : predicate;
|
|
58
|
-
}
|
package/src/runtime-options.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { existsSync as fileExistsSync } from 'node:fs';
|
|
2
|
-
import { extname, resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { createApplication } from './app/create-application.js';
|
|
5
|
-
import { SpaceKind } from './models.js';
|
|
6
|
-
import { resolveSetupPaths } from './setup/paths.js';
|
|
7
|
-
import { SqliteStore } from './storage/sqlite-store.js';
|
|
8
|
-
|
|
9
|
-
const LEGACY_LOCAL_DB_PATH = '.fuli/context.db';
|
|
10
|
-
const DEFAULT_PERSONAL_SPACE = '我';
|
|
11
|
-
const RETRY_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
12
|
-
const RETRY_DELAY_MS = 5;
|
|
13
|
-
const STORE_OPEN_TIMEOUT_MS = 5000;
|
|
14
|
-
|
|
15
|
-
export class RuntimeConfigurationError extends TypeError {}
|
|
16
|
-
|
|
17
|
-
export function resolveRuntimeOptions(args = [], env = process.env, {
|
|
18
|
-
cwd = process.cwd(),
|
|
19
|
-
existsSync = fileExistsSync,
|
|
20
|
-
setupPaths = {}
|
|
21
|
-
} = {}) {
|
|
22
|
-
const cliDbPath = runtimeValue(args, '--db');
|
|
23
|
-
const envDbPath = environmentValue(env, 'FULI_DB_PATH');
|
|
24
|
-
const systemDbPath = resolveSetupPaths({ env, ...setupPaths }).dbPath;
|
|
25
|
-
let defaultDbPath = systemDbPath;
|
|
26
|
-
if (cliDbPath === null && envDbPath === null) {
|
|
27
|
-
defaultDbPath = resolveDefaultDbPath({ cwd, existsSync, systemDbPath });
|
|
28
|
-
guardImplicitDefaultUpgrade({ cwd, existsSync });
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
dbPath: cliDbPath ?? envDbPath ?? defaultDbPath,
|
|
32
|
-
personalSpaceName: runtimeValue(args, '--personal-space') ??
|
|
33
|
-
environmentValue(env, 'FULI_PERSONAL_SPACE') ?? DEFAULT_PERSONAL_SPACE
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function resolveStore({ dbPath }) {
|
|
38
|
-
if (extname(dbPath).toLowerCase() === '.json') {
|
|
39
|
-
throw new RuntimeConfigurationError(legacyJsonMessage(dbPath));
|
|
40
|
-
}
|
|
41
|
-
return openSqliteStore(dbPath);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function quoteShellArgument(value, platform = process.platform) {
|
|
45
|
-
const argument = String(value);
|
|
46
|
-
if (platform === 'win32') {
|
|
47
|
-
if (argument.includes('"')) {
|
|
48
|
-
throw new TypeError('Windows paths cannot contain a double quote');
|
|
49
|
-
}
|
|
50
|
-
return `"${argument}"`;
|
|
51
|
-
}
|
|
52
|
-
return `'${argument.replaceAll("'", "'\\''")}'`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function resolveDefaultDbPath({ cwd, existsSync, systemDbPath }) {
|
|
56
|
-
if (existsSync(systemDbPath)) return systemDbPath;
|
|
57
|
-
if (existsSync(resolve(cwd, LEGACY_LOCAL_DB_PATH))) return LEGACY_LOCAL_DB_PATH;
|
|
58
|
-
return systemDbPath;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function guardImplicitDefaultUpgrade({ cwd, existsSync }) {
|
|
62
|
-
const legacyPath = resolve(cwd, '.fuli/context.json');
|
|
63
|
-
const localDatabasePath = resolve(cwd, LEGACY_LOCAL_DB_PATH);
|
|
64
|
-
if (existsSync(legacyPath) && !existsSync(localDatabasePath)) {
|
|
65
|
-
throw new RuntimeConfigurationError(legacyJsonMessage(legacyPath));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function legacyJsonMessage(source) {
|
|
70
|
-
return `JSON is a retired legacy format (${quoteShellArgument(source)}). ` +
|
|
71
|
-
'The current Fuli CLI no longer provides SQLite knowledge commands or JSON migration.';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function openLocalApplication({ dbPath, personalSpaceName = DEFAULT_PERSONAL_SPACE }) {
|
|
75
|
-
const store = resolveStore({ dbPath });
|
|
76
|
-
const app = createApplication({ store, activePersonalSpaceName: personalSpaceName });
|
|
77
|
-
try {
|
|
78
|
-
initializeLocalSpaces(app, store, personalSpaceName);
|
|
79
|
-
return app;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
app.close();
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function initializeLocalSpaces(app, store, personalSpaceName) {
|
|
87
|
-
return store.transaction(() => {
|
|
88
|
-
if (store.listSpaces().length === 0) {
|
|
89
|
-
if (personalSpaceName === DEFAULT_PERSONAL_SPACE) {
|
|
90
|
-
app.bootstrap();
|
|
91
|
-
} else {
|
|
92
|
-
const personal = app.createSpace(personalSpaceName, SpaceKind.PERSONAL);
|
|
93
|
-
const project = app.createSpace('工作', SpaceKind.PUBLIC);
|
|
94
|
-
app.subscribe(personal.id, project.id);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return app.requireActivePersonalSpace();
|
|
98
|
-
}, { mode: 'immediate' });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function runtimeValue(args, flag) {
|
|
102
|
-
const values = [];
|
|
103
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
104
|
-
if (args[index] !== flag) continue;
|
|
105
|
-
const value = args[index + 1];
|
|
106
|
-
if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
|
|
107
|
-
throw new TypeError(`Missing value for ${flag}`);
|
|
108
|
-
}
|
|
109
|
-
values.push(value);
|
|
110
|
-
}
|
|
111
|
-
if (values.length > 1) throw new TypeError(`Duplicate ${flag}`);
|
|
112
|
-
return values[0] ?? null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function environmentValue(env, name) {
|
|
116
|
-
const value = env?.[name];
|
|
117
|
-
if (value === undefined) return null;
|
|
118
|
-
if (typeof value !== 'string' || !value.trim()) {
|
|
119
|
-
throw new TypeError(`${name} must not be empty`);
|
|
120
|
-
}
|
|
121
|
-
return value;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function openSqliteStore(dbPath) {
|
|
125
|
-
const deadline = Date.now() + STORE_OPEN_TIMEOUT_MS;
|
|
126
|
-
while (true) {
|
|
127
|
-
try {
|
|
128
|
-
return new SqliteStore(dbPath);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (!isDatabaseBusy(error) || Date.now() >= deadline) throw error;
|
|
131
|
-
Atomics.wait(RETRY_BUFFER, 0, 0, RETRY_DELAY_MS);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isDatabaseBusy(error) {
|
|
137
|
-
return error?.code === 'SQLITE_BUSY' || error?.code === 'SQLITE_LOCKED';
|
|
138
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { detectSensitiveContent } from './sensitive-content.js';
|
|
2
|
-
|
|
3
|
-
export function isRestrictedEpisode(episode) {
|
|
4
|
-
return [
|
|
5
|
-
episode.sourceKind,
|
|
6
|
-
episode.body,
|
|
7
|
-
episode.sourceUri,
|
|
8
|
-
JSON.stringify(episode.metadata)
|
|
9
|
-
].some(hasSensitiveContent);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function hasSensitiveContent(text) {
|
|
13
|
-
return detectSensitiveContent(text).restricted;
|
|
14
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Sensitivity } from '../models.js';
|
|
2
|
-
import { detectSensitiveContent } from './sensitive-content.js';
|
|
3
|
-
|
|
4
|
-
export function isRestrictedFact(fact) {
|
|
5
|
-
return fact.sensitivity === Sensitivity.RESTRICTED ||
|
|
6
|
-
[
|
|
7
|
-
fact.subject,
|
|
8
|
-
fact.predicate,
|
|
9
|
-
fact.object
|
|
10
|
-
].some(hasSensitiveContent);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function hasSensitiveContent(text) {
|
|
14
|
-
return detectSensitiveContent(text).restricted;
|
|
15
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
|
|
2
|
-
import { detectSensitiveContent } from './sensitive-content.js';
|
|
3
|
-
|
|
4
|
-
export function assertSafeSourceMetadata(sourceKind, sourceUri, errorMessage) {
|
|
5
|
-
if ([sourceKind, sourceUri].some((text) => detectSensitiveContent(text).restricted)) {
|
|
6
|
-
throw new ApplicationError(ApplicationErrorCode.VALIDATION, errorMessage);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
export const MAX_EPISODE_EVIDENCE_BODY_BYTES = 16 * 1024;
|
|
2
|
-
export const MAX_EPISODE_METADATA_BYTES = 16 * 1024;
|
|
3
|
-
export const MAX_EPISODE_METADATA_FIELD_BYTES = 2048;
|
|
4
|
-
export const DEFAULT_CORRECTION_EVIDENCE_PER_FACT = 5;
|
|
5
|
-
export const MAX_CORRECTION_EVIDENCE_PER_FACT = 20;
|
|
6
|
-
|
|
7
|
-
export function normalizeEvidencePreviewOptions(options = {}) {
|
|
8
|
-
const maxBodyBytes = options.maxBodyBytes ?? MAX_EPISODE_EVIDENCE_BODY_BYTES;
|
|
9
|
-
if (
|
|
10
|
-
!Number.isInteger(maxBodyBytes) ||
|
|
11
|
-
maxBodyBytes < 0 ||
|
|
12
|
-
maxBodyBytes > MAX_EPISODE_EVIDENCE_BODY_BYTES
|
|
13
|
-
) {
|
|
14
|
-
throw new TypeError(
|
|
15
|
-
`Episode evidence body limit must be between 0 and ${MAX_EPISODE_EVIDENCE_BODY_BYTES}`
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
maxBodyBytes,
|
|
20
|
-
includeRestricted: options.includeRestricted === true
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function normalizeCorrectionEvidencePreviewOptions(options = {}) {
|
|
25
|
-
const normalized = normalizeEvidencePreviewOptions(options);
|
|
26
|
-
const maxCorrectionsPerFact = options.maxCorrectionsPerFact ??
|
|
27
|
-
DEFAULT_CORRECTION_EVIDENCE_PER_FACT;
|
|
28
|
-
if (
|
|
29
|
-
!Number.isInteger(maxCorrectionsPerFact) ||
|
|
30
|
-
maxCorrectionsPerFact < 1 ||
|
|
31
|
-
maxCorrectionsPerFact > MAX_CORRECTION_EVIDENCE_PER_FACT
|
|
32
|
-
) {
|
|
33
|
-
throw new TypeError(
|
|
34
|
-
`Correction evidence limit must be between 1 and ${MAX_CORRECTION_EVIDENCE_PER_FACT}`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return { ...normalized, maxCorrectionsPerFact };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function previewEpisode(episode, options, { correction = false } = {}) {
|
|
41
|
-
if (!episode) return null;
|
|
42
|
-
const { value: body, truncated: bodyTruncated } = clipUtf8(
|
|
43
|
-
episode.body,
|
|
44
|
-
options.maxBodyBytes
|
|
45
|
-
);
|
|
46
|
-
const metadata = correction
|
|
47
|
-
? previewCorrectionMetadata(episode.metadata)
|
|
48
|
-
: previewSourceMetadata(episode.metadata);
|
|
49
|
-
const truncatedFields = [];
|
|
50
|
-
if (bodyTruncated || episode.bodyTruncated) truncatedFields.push('body');
|
|
51
|
-
if (metadata.truncated || episode.metadataTruncated) truncatedFields.push('metadata');
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
id: episode.id,
|
|
55
|
-
spaceId: episode.spaceId,
|
|
56
|
-
sourceKind: episode.sourceKind,
|
|
57
|
-
body,
|
|
58
|
-
sourceUri: episode.sourceUri,
|
|
59
|
-
metadata: metadata.value,
|
|
60
|
-
createdAt: episode.createdAt,
|
|
61
|
-
truncatedFields
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function clipUtf8(value, maxBytes) {
|
|
66
|
-
const text = typeof value === 'string' ? value : '';
|
|
67
|
-
const encoded = Buffer.from(text, 'utf8');
|
|
68
|
-
if (encoded.length <= maxBytes) return { value: text, truncated: false };
|
|
69
|
-
let end = maxBytes;
|
|
70
|
-
while (end > 0 && (encoded[end] & 0xc0) === 0x80) end -= 1;
|
|
71
|
-
return { value: encoded.subarray(0, end).toString('utf8'), truncated: true };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function previewSourceMetadata(metadata) {
|
|
75
|
-
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
76
|
-
return { value: {}, truncated: metadata !== undefined };
|
|
77
|
-
}
|
|
78
|
-
const serialized = JSON.stringify(metadata);
|
|
79
|
-
if (Buffer.byteLength(serialized, 'utf8') > MAX_EPISODE_METADATA_BYTES) {
|
|
80
|
-
return { value: {}, truncated: true };
|
|
81
|
-
}
|
|
82
|
-
return { value: JSON.parse(serialized), truncated: false };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function previewCorrectionMetadata(metadata) {
|
|
86
|
-
const source = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
87
|
-
? metadata
|
|
88
|
-
: {};
|
|
89
|
-
const value = {};
|
|
90
|
-
let truncated = Object.keys(source).some(
|
|
91
|
-
(key) => !['kind', 'factId', 'action'].includes(key)
|
|
92
|
-
);
|
|
93
|
-
for (const key of ['kind', 'factId', 'action']) {
|
|
94
|
-
if (typeof source[key] !== 'string') continue;
|
|
95
|
-
const clipped = clipUtf8(source[key], MAX_EPISODE_METADATA_FIELD_BYTES);
|
|
96
|
-
value[key] = clipped.value;
|
|
97
|
-
truncated ||= clipped.truncated;
|
|
98
|
-
}
|
|
99
|
-
return { value, truncated };
|
|
100
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function compareSearchFacts(left, right) {
|
|
2
|
-
return compareText(right.validAt, left.validAt) || compareText(left.id, right.id);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function searchTextSortKey(value) {
|
|
6
|
-
const text = String(value ?? '');
|
|
7
|
-
const key = Buffer.allocUnsafe(text.length * 2);
|
|
8
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
9
|
-
key.writeUInt16BE(text.charCodeAt(index), index * 2);
|
|
10
|
-
}
|
|
11
|
-
return key;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function compareText(left, right) {
|
|
15
|
-
const first = String(left ?? '');
|
|
16
|
-
const second = String(right ?? '');
|
|
17
|
-
return first === second ? 0 : (first < second ? -1 : 1);
|
|
18
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { isRestrictedEpisode } from '../../security/episode-visibility.js';
|
|
2
|
-
import {
|
|
3
|
-
normalizeCorrectionEvidencePreviewOptions,
|
|
4
|
-
normalizeEvidencePreviewOptions,
|
|
5
|
-
previewEpisode
|
|
6
|
-
} from '../episode-evidence-preview.js';
|
|
7
|
-
|
|
8
|
-
export function fileEpisodeEvidencePreview(index, spaceId, episodeId, options = {}) {
|
|
9
|
-
const normalized = normalizeEvidencePreviewOptions(options);
|
|
10
|
-
const episode = index.get(episodeId);
|
|
11
|
-
if (!isVisibleEpisode(episode, spaceId, normalized.includeRestricted)) return null;
|
|
12
|
-
return previewEpisode(episode, normalized);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function fileCorrectionEpisodeEvidencePreviews(
|
|
16
|
-
index,
|
|
17
|
-
spaceId,
|
|
18
|
-
factIds,
|
|
19
|
-
options = {}
|
|
20
|
-
) {
|
|
21
|
-
const normalized = normalizeCorrectionEvidencePreviewOptions(options);
|
|
22
|
-
return index.correctionWindows(spaceId, factIds, {
|
|
23
|
-
includeRestricted: normalized.includeRestricted,
|
|
24
|
-
limit: normalized.maxCorrectionsPerFact
|
|
25
|
-
}).map((group) => ({
|
|
26
|
-
factId: group.factId,
|
|
27
|
-
episodes: group.episodes.map(
|
|
28
|
-
(episode) => previewEpisode(episode, normalized, { correction: true })
|
|
29
|
-
),
|
|
30
|
-
truncated: group.truncated
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function isVisibleEpisode(episode, spaceId, includeRestricted) {
|
|
35
|
-
return episode?.spaceId === spaceId &&
|
|
36
|
-
(includeRestricted || !isRestrictedEpisode(episode));
|
|
37
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { isRestrictedEpisode } from '../../security/episode-visibility.js';
|
|
2
|
-
|
|
3
|
-
export class FileEpisodeIndex {
|
|
4
|
-
#byId = new Map();
|
|
5
|
-
#correctionsBySpace = new Map();
|
|
6
|
-
#safeCorrectionsBySpace = new Map();
|
|
7
|
-
|
|
8
|
-
constructor(episodes) {
|
|
9
|
-
this.rebuild(episodes);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
rebuild(episodes) {
|
|
13
|
-
this.#byId = new Map();
|
|
14
|
-
this.#correctionsBySpace = new Map();
|
|
15
|
-
this.#safeCorrectionsBySpace = new Map();
|
|
16
|
-
for (const episode of episodes) this.add(episode);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
add(episode) {
|
|
20
|
-
this.#byId.set(episode.id, episode);
|
|
21
|
-
const factId = episode.metadata?.kind === 'lens_correction'
|
|
22
|
-
? episode.metadata.factId
|
|
23
|
-
: null;
|
|
24
|
-
if (typeof factId !== 'string') return;
|
|
25
|
-
|
|
26
|
-
addCorrection(this.#correctionsBySpace, episode, factId);
|
|
27
|
-
if (!isRestrictedEpisode(episode)) {
|
|
28
|
-
addCorrection(this.#safeCorrectionsBySpace, episode, factId);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
get(id) {
|
|
33
|
-
return this.#byId.get(id) ?? null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
correctionWindows(spaceId, factIds, { includeRestricted, limit }) {
|
|
37
|
-
const index = includeRestricted
|
|
38
|
-
? this.#correctionsBySpace
|
|
39
|
-
: this.#safeCorrectionsBySpace;
|
|
40
|
-
const byFact = index.get(spaceId) ?? new Map();
|
|
41
|
-
return [...new Set(factIds)].map((factId) => {
|
|
42
|
-
const episodes = byFact.get(factId) ?? [];
|
|
43
|
-
return {
|
|
44
|
-
factId,
|
|
45
|
-
episodes: episodes.slice(Math.max(0, episodes.length - limit)).reverse(),
|
|
46
|
-
truncated: episodes.length > limit
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function addCorrection(index, episode, factId) {
|
|
53
|
-
const byFact = index.get(episode.spaceId) ?? new Map();
|
|
54
|
-
const episodes = byFact.get(factId) ?? [];
|
|
55
|
-
episodes.push(episode);
|
|
56
|
-
episodes.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
57
|
-
byFact.set(factId, episodes);
|
|
58
|
-
index.set(episode.spaceId, byFact);
|
|
59
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { isCurrentFact } from '../../models.js';
|
|
2
|
-
import { isRestrictedEpisode } from '../../security/episode-visibility.js';
|
|
3
|
-
import { isRestrictedFact } from '../../security/fact-visibility.js';
|
|
4
|
-
import { compareSearchFacts } from '../fact-search-order.js';
|
|
5
|
-
|
|
6
|
-
export function searchFileFacts(facts, episodeIndex, spaceIds, query, options = {}) {
|
|
7
|
-
return searchFileFactsPage(facts, episodeIndex, spaceIds, query, options).facts;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function searchFileFactsPage(facts, episodeIndex, spaceIds, query, options = {}) {
|
|
11
|
-
const search = {
|
|
12
|
-
includeHistorical: options.includeHistorical ?? false,
|
|
13
|
-
scope: options.scope ?? null,
|
|
14
|
-
limit: options.limit ?? null,
|
|
15
|
-
excludeRestricted: options.excludeRestricted === true,
|
|
16
|
-
safeSourceSpaceId: options.safeSourceSpaceId ?? null,
|
|
17
|
-
maxObjectBytes: options.maxObjectBytes ?? null,
|
|
18
|
-
query: query.toLowerCase()
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
if (search.limit !== null && (!Number.isInteger(search.limit) || search.limit <= 0)) {
|
|
22
|
-
throw new TypeError('Fact search limit must be a positive integer');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const selected = [];
|
|
26
|
-
let objectLimitTruncated = false;
|
|
27
|
-
for (const fact of facts) {
|
|
28
|
-
if (!matchesSearchWithoutObjectLimit(fact, episodeIndex, spaceIds, search)) continue;
|
|
29
|
-
if (
|
|
30
|
-
search.maxObjectBytes !== null &&
|
|
31
|
-
Buffer.byteLength(fact.object, 'utf8') > search.maxObjectBytes
|
|
32
|
-
) {
|
|
33
|
-
objectLimitTruncated = true;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
selected.push(fact);
|
|
37
|
-
if (search.limit === null) continue;
|
|
38
|
-
selected.sort(compareSearchFacts);
|
|
39
|
-
if (selected.length > search.limit) selected.pop();
|
|
40
|
-
}
|
|
41
|
-
return { facts: selected, objectLimitTruncated };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function matchesSearchWithoutObjectLimit(fact, episodeIndex, spaceIds, search) {
|
|
45
|
-
if (!spaceIds.includes(fact.spaceId)) return false;
|
|
46
|
-
if (search.scope !== null && fact.scope !== search.scope) return false;
|
|
47
|
-
if (!search.includeHistorical && !isCurrentFact(fact)) return false;
|
|
48
|
-
if (search.excludeRestricted && isRestrictedFact(fact)) return false;
|
|
49
|
-
if (search.safeSourceSpaceId !== null) {
|
|
50
|
-
const source = episodeIndex.get(fact.sourceEpisodeId);
|
|
51
|
-
if (
|
|
52
|
-
!source ||
|
|
53
|
-
source.spaceId !== search.safeSourceSpaceId ||
|
|
54
|
-
isRestrictedEpisode(source)
|
|
55
|
-
) return false;
|
|
56
|
-
}
|
|
57
|
-
return `${fact.subject} ${fact.predicate} ${fact.object}`
|
|
58
|
-
.toLowerCase()
|
|
59
|
-
.includes(search.query);
|
|
60
|
-
}
|