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,29 @@
|
|
|
1
|
+
// Keep local API request buffering at or below a fixed 64 KiB budget.
|
|
2
|
+
export const MAX_JSON_BODY_BYTES = 64 * 1024;
|
|
3
|
+
|
|
4
|
+
export class JsonBodyTooLargeError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(`JSON request body exceeds ${MAX_JSON_BODY_BYTES} bytes`);
|
|
7
|
+
this.name = 'JsonBodyTooLargeError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function readJson(request) {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
let receivedBytes = 0;
|
|
14
|
+
for await (const chunk of request) {
|
|
15
|
+
receivedBytes += chunk.length;
|
|
16
|
+
if (receivedBytes > MAX_JSON_BODY_BYTES) {
|
|
17
|
+
chunks.length = 0;
|
|
18
|
+
throw new JsonBodyTooLargeError();
|
|
19
|
+
}
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
}
|
|
22
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
23
|
+
return raw ? JSON.parse(raw) : {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sendJson(response, status, body) {
|
|
27
|
+
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
28
|
+
response.end(JSON.stringify(body));
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const PROJECT_ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
|
|
6
|
+
export const WEB_ROOT = join(PROJECT_ROOT, 'dist', 'web');
|
|
7
|
+
const MIME_TYPES = {
|
|
8
|
+
'.html': 'text/html; charset=utf-8',
|
|
9
|
+
'.css': 'text/css; charset=utf-8',
|
|
10
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
11
|
+
'.map': 'application/json; charset=utf-8',
|
|
12
|
+
'.json': 'application/json; charset=utf-8',
|
|
13
|
+
'.png': 'image/png',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.webp': 'image/webp',
|
|
16
|
+
'.woff2': 'font/woff2'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function serveStatic(pathname, response) {
|
|
20
|
+
if (pathname === '/favicon.ico') {
|
|
21
|
+
response.writeHead(204);
|
|
22
|
+
response.end();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const relativePath = pathname === '/' ? 'index.html' : pathname.slice(1);
|
|
27
|
+
const requestedFile = normalize(join(WEB_ROOT, relativePath));
|
|
28
|
+
const pathFromRoot = relative(WEB_ROOT, requestedFile);
|
|
29
|
+
if (pathFromRoot === '..' || pathFromRoot.startsWith(`..${sep}`) ||
|
|
30
|
+
isAbsolute(pathFromRoot)) {
|
|
31
|
+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
32
|
+
response.end('Not found');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const filePath = existsSync(requestedFile)
|
|
37
|
+
? requestedFile
|
|
38
|
+
: shouldServeApplication(pathname)
|
|
39
|
+
? join(WEB_ROOT, 'index.html')
|
|
40
|
+
: null;
|
|
41
|
+
if (!filePath || !existsSync(filePath)) {
|
|
42
|
+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
43
|
+
response.end('Not found');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
response.writeHead(200, {
|
|
48
|
+
'content-type': MIME_TYPES[extname(filePath)] ?? 'application/octet-stream'
|
|
49
|
+
});
|
|
50
|
+
response.end(readFileSync(filePath));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shouldServeApplication(pathname) {
|
|
54
|
+
return extname(pathname) === '' && !pathname.startsWith('/api/');
|
|
55
|
+
}
|
package/src/ingestion.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { classifyEpisode } from './classifier.js';
|
|
2
|
+
import { extractFactSpecs } from './extractor.js';
|
|
3
|
+
import { FactService } from './facts/fact-service.js';
|
|
4
|
+
import { LensService } from './lens/lens-service.js';
|
|
5
|
+
import { ApplicationError, ApplicationErrorCode } from './app/application-error.js';
|
|
6
|
+
import { CandidateStatus, FactScope, PublishRoute, SpaceKind } from './models.js';
|
|
7
|
+
import { detectSensitiveContent } from './security/sensitive-content.js';
|
|
8
|
+
import { assertSafeSourceMetadata } from './security/source-metadata-policy.js';
|
|
9
|
+
import { PublicationService } from './publication/publication-service.js';
|
|
10
|
+
|
|
11
|
+
export class IngestionService {
|
|
12
|
+
constructor(store, {
|
|
13
|
+
facts = new FactService(store),
|
|
14
|
+
lens = new LensService(store),
|
|
15
|
+
publication = new PublicationService(store)
|
|
16
|
+
} = {}) {
|
|
17
|
+
this.store = store;
|
|
18
|
+
this.facts = facts;
|
|
19
|
+
this.lens = lens;
|
|
20
|
+
this.publication = publication;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
remember(input) {
|
|
24
|
+
this.#assertSafeInput(input);
|
|
25
|
+
return this.store.transaction(
|
|
26
|
+
() => this.rememberInCurrentTransaction(input),
|
|
27
|
+
{ mode: 'immediate' }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
rememberInCurrentTransaction({
|
|
32
|
+
personalSpaceId,
|
|
33
|
+
sourceKind,
|
|
34
|
+
body,
|
|
35
|
+
targetSpaceId = null,
|
|
36
|
+
sourceUri = null
|
|
37
|
+
}) {
|
|
38
|
+
this.#assertSafeInput({ body, sourceKind, sourceUri });
|
|
39
|
+
const route = classifyEpisode({ sourceKind, body });
|
|
40
|
+
|
|
41
|
+
if (route === PublishRoute.PERSONAL) {
|
|
42
|
+
const result = this.lens.rememberUserFactsFromSourceInCurrentTransaction({
|
|
43
|
+
personalSpaceId,
|
|
44
|
+
subject: this.#spaceName(personalSpaceId),
|
|
45
|
+
specs: extractFactSpecs(this.#spaceName(personalSpaceId), body),
|
|
46
|
+
sourceText: body,
|
|
47
|
+
sourceKind,
|
|
48
|
+
sourceUri
|
|
49
|
+
});
|
|
50
|
+
return { route, ...result };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const personalEpisode = this.store.addEpisode(personalSpaceId, sourceKind, body, sourceUri);
|
|
54
|
+
|
|
55
|
+
if (route === PublishRoute.PUBLIC && targetSpaceId) {
|
|
56
|
+
const targetEpisode = this.store.addEpisode(targetSpaceId, sourceKind, body, sourceUri);
|
|
57
|
+
const facts = this.facts.writeSpecs({
|
|
58
|
+
spaceId: targetSpaceId,
|
|
59
|
+
subject: this.#spaceName(targetSpaceId),
|
|
60
|
+
episodeId: targetEpisode.id,
|
|
61
|
+
specs: extractFactSpecs(this.#spaceName(targetSpaceId), body),
|
|
62
|
+
scope: FactScope.PUBLIC
|
|
63
|
+
});
|
|
64
|
+
const publication = facts.length === 0
|
|
65
|
+
? null
|
|
66
|
+
: this.publication.prepareInCurrentTransaction({
|
|
67
|
+
spaceId: targetSpaceId,
|
|
68
|
+
episode: targetEpisode,
|
|
69
|
+
facts
|
|
70
|
+
});
|
|
71
|
+
return { route, episode: targetEpisode, publication };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const candidate = this.store.addCandidate({
|
|
75
|
+
personalSpaceId,
|
|
76
|
+
targetSpaceId,
|
|
77
|
+
episodeId: personalEpisode.id,
|
|
78
|
+
reason: 'not safe enough to publish automatically',
|
|
79
|
+
status: CandidateStatus.PENDING
|
|
80
|
+
});
|
|
81
|
+
return { route: PublishRoute.CANDIDATE, episode: personalEpisode, candidate };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
publishConfirmed(input) {
|
|
85
|
+
this.#assertSafeInput(input);
|
|
86
|
+
return this.store.transaction(
|
|
87
|
+
() => this.publishConfirmedInCurrentTransaction(input),
|
|
88
|
+
{ mode: 'immediate' }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
publishConfirmedInCurrentTransaction({ targetSpaceId, sourceKind, body, sourceUri = null }) {
|
|
93
|
+
this.#assertSafeInput({ body, sourceKind, sourceUri });
|
|
94
|
+
const targetEpisode = this.store.addEpisode(targetSpaceId, sourceKind, body, sourceUri);
|
|
95
|
+
const result = this.confirmEpisodeInCurrentTransaction({
|
|
96
|
+
spaceId: targetSpaceId,
|
|
97
|
+
episode: targetEpisode
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
confirmEpisode(input) {
|
|
103
|
+
this.#assertSafeEpisode(input.episode);
|
|
104
|
+
return this.store.transaction(
|
|
105
|
+
() => this.confirmEpisodeInCurrentTransaction(input),
|
|
106
|
+
{ mode: 'immediate' }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
confirmEpisodeInCurrentTransaction({ spaceId, episode }) {
|
|
111
|
+
this.#assertSafeEpisode(episode);
|
|
112
|
+
const subject = this.#spaceName(spaceId);
|
|
113
|
+
const specs = extractFactSpecs(subject, episode.body);
|
|
114
|
+
if (this.store.getSpace(spaceId)?.kind === SpaceKind.PERSONAL) {
|
|
115
|
+
return this.lens.writeUserFactsForEpisodeInCurrentTransaction({
|
|
116
|
+
personalSpaceId: spaceId,
|
|
117
|
+
subject,
|
|
118
|
+
specs,
|
|
119
|
+
episode
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const facts = this.facts.writeSpecs({
|
|
123
|
+
spaceId,
|
|
124
|
+
subject,
|
|
125
|
+
episodeId: episode.id,
|
|
126
|
+
specs,
|
|
127
|
+
scope: FactScope.PUBLIC
|
|
128
|
+
});
|
|
129
|
+
const publication = facts.length === 0
|
|
130
|
+
? null
|
|
131
|
+
: this.publication.prepareInCurrentTransaction({ spaceId, episode, facts });
|
|
132
|
+
return { episode, publication };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#spaceName(spaceId) {
|
|
136
|
+
return this.store.getSpace(spaceId)?.name ?? spaceId;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#assertSafeInput({ body, sourceKind, sourceUri }) {
|
|
140
|
+
this.#assertSafeBody(body);
|
|
141
|
+
assertSafeSourceMetadata(
|
|
142
|
+
sourceKind,
|
|
143
|
+
sourceUri,
|
|
144
|
+
'Sensitive source metadata is not allowed in ingestion'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#assertSafeEpisode(episode) {
|
|
149
|
+
this.#assertSafeInput(episode);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#assertSafeBody(body) {
|
|
153
|
+
if (detectSensitiveContent(body).restricted) {
|
|
154
|
+
throw new ApplicationError(
|
|
155
|
+
ApplicationErrorCode.VALIDATION,
|
|
156
|
+
'Sensitive content is not allowed in ingestion'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { nowIso, FactScope, FactStatus, isCurrentFact } from '../models.js';
|
|
2
|
+
import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
|
|
3
|
+
import { assertStorePort } from '../storage/store-port.js';
|
|
4
|
+
import { writePersonalFact } from './lens-fact-writer.js';
|
|
5
|
+
import { assertSafeLensTexts, requirePersonalSpace } from './lens-input.js';
|
|
6
|
+
|
|
7
|
+
const ACTIONS = new Set(['replace', 'reject', 'deprecate']);
|
|
8
|
+
const CORRECTABLE_STATUSES = new Set([
|
|
9
|
+
FactStatus.CONFIRMED,
|
|
10
|
+
FactStatus.OBSERVED,
|
|
11
|
+
FactStatus.SUGGESTED
|
|
12
|
+
]);
|
|
13
|
+
const IMMEDIATE = { mode: 'immediate' };
|
|
14
|
+
|
|
15
|
+
export class LensCorrectionService {
|
|
16
|
+
constructor(store) {
|
|
17
|
+
assertStorePort(store);
|
|
18
|
+
this.store = store;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
correctUserFact({ personalSpaceId, factId, action, value, sourceText, sourceKind = 'correction' }) {
|
|
22
|
+
if (!ACTIONS.has(action)) {
|
|
23
|
+
throw new ApplicationError(ApplicationErrorCode.VALIDATION, `Unknown correction action: ${action}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
requirePersonalSpace(this.store, personalSpaceId);
|
|
27
|
+
assertSafeLensTexts([sourceText]);
|
|
28
|
+
if (action === 'replace') assertSafeLensTexts([value]);
|
|
29
|
+
|
|
30
|
+
const original = this.#requireCurrentFact(personalSpaceId, factId);
|
|
31
|
+
if (action === 'replace') assertSafeLensTexts([original.subject, original.predicate]);
|
|
32
|
+
|
|
33
|
+
return this.store.transaction(() => {
|
|
34
|
+
requirePersonalSpace(this.store, personalSpaceId);
|
|
35
|
+
const current = this.#requireCurrentFact(personalSpaceId, factId);
|
|
36
|
+
assertSafeLensTexts([sourceText]);
|
|
37
|
+
if (action === 'replace') assertSafeLensTexts([current.subject, current.predicate, value]);
|
|
38
|
+
|
|
39
|
+
const episode = this.store.addEpisode(
|
|
40
|
+
personalSpaceId,
|
|
41
|
+
sourceKind,
|
|
42
|
+
sourceText,
|
|
43
|
+
null,
|
|
44
|
+
{ kind: 'lens_correction', factId: current.id, action }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (action === 'replace') {
|
|
48
|
+
const fact = writePersonalFact(this.store, {
|
|
49
|
+
spaceId: personalSpaceId,
|
|
50
|
+
subject: current.subject,
|
|
51
|
+
predicate: current.predicate,
|
|
52
|
+
object: value,
|
|
53
|
+
sourceEpisodeId: episode.id,
|
|
54
|
+
status: FactStatus.CONFIRMED,
|
|
55
|
+
sensitivity: current.sensitivity,
|
|
56
|
+
confidence: current.confidence,
|
|
57
|
+
scope: FactScope.PERSONAL
|
|
58
|
+
});
|
|
59
|
+
this.store.invalidateFact(current.id, fact.id);
|
|
60
|
+
return { episode, fact, replacedFact: this.store.getFact(current.id) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fact = this.store.updateFact(current.id, {
|
|
64
|
+
status: action === 'reject' ? FactStatus.REJECTED : FactStatus.DEPRECATED,
|
|
65
|
+
invalidAt: nowIso()
|
|
66
|
+
});
|
|
67
|
+
return { episode, fact };
|
|
68
|
+
}, IMMEDIATE);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#requireCurrentFact(personalSpaceId, factId) {
|
|
72
|
+
const fact = this.store.getFact(factId);
|
|
73
|
+
if (!fact || fact.spaceId !== personalSpaceId || fact.scope !== FactScope.PERSONAL) {
|
|
74
|
+
throw new ApplicationError(ApplicationErrorCode.NOT_FOUND, `Lens fact not found: ${factId}`);
|
|
75
|
+
}
|
|
76
|
+
if (!isCurrentFact(fact) || !CORRECTABLE_STATUSES.has(fact.status)) {
|
|
77
|
+
throw new ApplicationError(ApplicationErrorCode.VALIDATION, `Lens fact is not current: ${factId}`);
|
|
78
|
+
}
|
|
79
|
+
return fact;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { FactScope, FactStatus } from '../models.js';
|
|
2
|
+
|
|
3
|
+
const STATUS_RANK = Object.freeze({
|
|
4
|
+
[FactStatus.SUGGESTED]: 1,
|
|
5
|
+
[FactStatus.OBSERVED]: 2,
|
|
6
|
+
[FactStatus.CONFIRMED]: 3
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function writePersonalFact(store, input) {
|
|
10
|
+
const equivalent = equivalentCurrentFacts(store, input);
|
|
11
|
+
const fact = store.addFact({
|
|
12
|
+
spaceId: input.spaceId,
|
|
13
|
+
subject: input.subject,
|
|
14
|
+
predicate: input.predicate,
|
|
15
|
+
object: input.object,
|
|
16
|
+
sourceEpisodeId: input.sourceEpisodeId,
|
|
17
|
+
status: input.status,
|
|
18
|
+
confidence: input.confidence,
|
|
19
|
+
sensitivity: input.sensitivity,
|
|
20
|
+
scope: FactScope.PERSONAL
|
|
21
|
+
});
|
|
22
|
+
const candidates = [...equivalent, fact];
|
|
23
|
+
const winner = candidates.reduce(strongerOrLater);
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (candidate.id !== winner.id) store.invalidateFact(candidate.id, winner.id);
|
|
26
|
+
}
|
|
27
|
+
return store.getFact(fact.id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function writePersonalFactSpecs(store, input) {
|
|
31
|
+
const facts = [];
|
|
32
|
+
for (const spec of input.specs) {
|
|
33
|
+
if (spec.kind === 'replacement') {
|
|
34
|
+
facts.push(...writeReplacement(store, input, spec));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
facts.push(writePersonalFact(store, {
|
|
38
|
+
...input,
|
|
39
|
+
subject: spec.subject ?? input.subject,
|
|
40
|
+
predicate: spec.predicate,
|
|
41
|
+
object: spec.object,
|
|
42
|
+
status: FactStatus.CONFIRMED
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
return facts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeReplacement(store, input, spec) {
|
|
49
|
+
const subject = spec.subject ?? input.subject;
|
|
50
|
+
const oldFacts = store.currentFacts(input.spaceId).filter(
|
|
51
|
+
(fact) => fact.subject === subject && fact.object === spec.oldValue
|
|
52
|
+
);
|
|
53
|
+
const predicates = oldFacts.length
|
|
54
|
+
? [...new Set(oldFacts.map((fact) => fact.predicate))]
|
|
55
|
+
: ['has_replacement'];
|
|
56
|
+
return predicates.map((predicate) => {
|
|
57
|
+
const fact = writePersonalFact(store, {
|
|
58
|
+
...input,
|
|
59
|
+
subject,
|
|
60
|
+
predicate,
|
|
61
|
+
object: spec.newValue,
|
|
62
|
+
status: FactStatus.CONFIRMED
|
|
63
|
+
});
|
|
64
|
+
for (const previous of oldFacts.filter((item) => item.predicate === predicate)) {
|
|
65
|
+
store.invalidateFact(previous.id, fact.id);
|
|
66
|
+
}
|
|
67
|
+
return fact;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function equivalentCurrentFacts(store, input) {
|
|
72
|
+
return store.currentFacts(input.spaceId).filter((fact) =>
|
|
73
|
+
fact.subject === input.subject &&
|
|
74
|
+
fact.predicate === input.predicate &&
|
|
75
|
+
fact.object === input.object
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function strongerOrLater(current, candidate) {
|
|
80
|
+
return rank(candidate.status) >= rank(current.status) ? candidate : current;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rank(status) {
|
|
84
|
+
return STATUS_RANK[status] ?? 0;
|
|
85
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
|
|
2
|
+
import { Sensitivity, SpaceKind } from '../models.js';
|
|
3
|
+
import { detectSensitiveContent } from '../security/sensitive-content.js';
|
|
4
|
+
|
|
5
|
+
export function validateLensWriteInput(
|
|
6
|
+
store,
|
|
7
|
+
{ personalSpaceId, texts, sensitivity, confidence }
|
|
8
|
+
) {
|
|
9
|
+
requirePersonalSpace(store, personalSpaceId);
|
|
10
|
+
assertSafeLensTexts(texts);
|
|
11
|
+
if (!Object.values(Sensitivity).includes(sensitivity)) {
|
|
12
|
+
throw new ApplicationError(
|
|
13
|
+
ApplicationErrorCode.VALIDATION,
|
|
14
|
+
`Invalid sensitivity: ${sensitivity}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (
|
|
18
|
+
typeof confidence !== 'number' ||
|
|
19
|
+
!Number.isFinite(confidence) ||
|
|
20
|
+
confidence < 0 ||
|
|
21
|
+
confidence > 1
|
|
22
|
+
) {
|
|
23
|
+
throw new ApplicationError(
|
|
24
|
+
ApplicationErrorCode.VALIDATION,
|
|
25
|
+
'Confidence must be between 0 and 1'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function requirePersonalSpace(store, spaceId) {
|
|
31
|
+
const space = store.getSpace(spaceId);
|
|
32
|
+
if (!space) {
|
|
33
|
+
throw new ApplicationError(
|
|
34
|
+
ApplicationErrorCode.NOT_FOUND,
|
|
35
|
+
`Personal space not found: ${spaceId}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (space.kind !== SpaceKind.PERSONAL) {
|
|
39
|
+
throw new ApplicationError(
|
|
40
|
+
ApplicationErrorCode.VALIDATION,
|
|
41
|
+
`Space is not personal: ${spaceId}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return space;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assertSafeLensTexts(texts) {
|
|
48
|
+
for (const text of texts) {
|
|
49
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
50
|
+
throw new ApplicationError(
|
|
51
|
+
ApplicationErrorCode.VALIDATION,
|
|
52
|
+
'Lens text must be a nonempty string'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (detectSensitiveContent(text).restricted) {
|
|
56
|
+
throw new ApplicationError(
|
|
57
|
+
ApplicationErrorCode.VALIDATION,
|
|
58
|
+
'Sensitive content is not allowed in Personal Lens'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { FactScope, FactStatus, isCurrentFact } from '../models.js';
|
|
2
|
+
import { isRestrictedEpisode } from '../security/episode-visibility.js';
|
|
3
|
+
import { isRestrictedFact } from '../security/fact-visibility.js';
|
|
4
|
+
|
|
5
|
+
export function isInjectableFact(
|
|
6
|
+
fact,
|
|
7
|
+
{ includeObserved, includeSuggested, includeRestricted }
|
|
8
|
+
) {
|
|
9
|
+
if (fact.invalidAt) return false;
|
|
10
|
+
if (fact.status === FactStatus.CONFIRMED) return isVisibleFact(fact, includeRestricted);
|
|
11
|
+
if (fact.status === FactStatus.OBSERVED && includeObserved) {
|
|
12
|
+
return isVisibleFact(fact, includeRestricted);
|
|
13
|
+
}
|
|
14
|
+
if (fact.status === FactStatus.SUGGESTED && includeSuggested) {
|
|
15
|
+
return isVisibleFact(fact, includeRestricted);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isSearchableFact(fact, { includeHistorical, includeRestricted }) {
|
|
21
|
+
return (includeHistorical || isCurrentFact(fact)) && isVisibleFact(fact, includeRestricted);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildCorrectionIndex(groups, personalSpaceId, includeRestricted) {
|
|
25
|
+
const index = new Map();
|
|
26
|
+
for (const group of groups) {
|
|
27
|
+
if (typeof group.factId !== 'string') continue;
|
|
28
|
+
const episodes = group.episodes.filter((episode) =>
|
|
29
|
+
episode.spaceId === personalSpaceId &&
|
|
30
|
+
episode.metadata?.kind === 'lens_correction' &&
|
|
31
|
+
episode.metadata?.factId === group.factId &&
|
|
32
|
+
(includeRestricted || !isRestrictedEpisode(episode))
|
|
33
|
+
);
|
|
34
|
+
index.set(group.factId, {
|
|
35
|
+
episodes,
|
|
36
|
+
truncated: group.truncated === true || episodes.length < group.episodes.length
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return index;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function projectSearchFact(
|
|
43
|
+
store,
|
|
44
|
+
fact,
|
|
45
|
+
evidenceEpisode,
|
|
46
|
+
personalSpaceId,
|
|
47
|
+
correctionIndex,
|
|
48
|
+
includeRestricted
|
|
49
|
+
) {
|
|
50
|
+
const sourceEpisode = safePersonalEpisode(
|
|
51
|
+
evidenceEpisode,
|
|
52
|
+
personalSpaceId,
|
|
53
|
+
includeRestricted
|
|
54
|
+
);
|
|
55
|
+
const replacement = fact.replacedByFactId ? store.getFact(fact.replacedByFactId) : null;
|
|
56
|
+
const replacementFact = replacement?.spaceId === personalSpaceId &&
|
|
57
|
+
replacement.scope === FactScope.PERSONAL &&
|
|
58
|
+
isVisibleFact(replacement, includeRestricted)
|
|
59
|
+
? replacement
|
|
60
|
+
: null;
|
|
61
|
+
return {
|
|
62
|
+
fact: projectSearchFactDto(fact, sourceEpisode, replacementFact),
|
|
63
|
+
current: isCurrentFact(fact),
|
|
64
|
+
sourceEpisode,
|
|
65
|
+
replacementFact,
|
|
66
|
+
correctionEpisodes: correctionIndex.get(fact.id)?.episodes ?? [],
|
|
67
|
+
correctionEpisodesTruncated: correctionIndex.get(fact.id)?.truncated ?? false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function projectSearchFactDto(fact, sourceEpisode, replacementFact) {
|
|
72
|
+
return {
|
|
73
|
+
...fact,
|
|
74
|
+
sourceEpisodeId: sourceEpisode?.id ?? null,
|
|
75
|
+
replacedByFactId: replacementFact?.id ?? null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isVisibleFact(fact, includeRestricted) {
|
|
80
|
+
return includeRestricted === true || !isRestrictedFact(fact);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function safePersonalEpisode(episode, personalSpaceId, includeRestricted) {
|
|
84
|
+
if (!episode || episode.spaceId !== personalSpaceId) return null;
|
|
85
|
+
if (!includeRestricted && isRestrictedEpisode(episode)) return null;
|
|
86
|
+
return episode;
|
|
87
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FactStatus } from '../models.js';
|
|
2
|
+
|
|
3
|
+
export { compareSearchFacts } from '../storage/fact-search-order.js';
|
|
4
|
+
|
|
5
|
+
export const BOUNDARY_PREDICATES = Object.freeze([
|
|
6
|
+
'has_boundary',
|
|
7
|
+
'has_access_boundary',
|
|
8
|
+
'does_not_want',
|
|
9
|
+
'avoids',
|
|
10
|
+
'forbids'
|
|
11
|
+
]);
|
|
12
|
+
export const BOUNDARY_PREFIXES = Object.freeze([
|
|
13
|
+
'boundary_',
|
|
14
|
+
'does_not_',
|
|
15
|
+
'avoids_',
|
|
16
|
+
'forbids_',
|
|
17
|
+
'must_not_'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const STATUS_RANK = Object.freeze({
|
|
21
|
+
[FactStatus.SUGGESTED]: 1,
|
|
22
|
+
[FactStatus.OBSERVED]: 2,
|
|
23
|
+
[FactStatus.CONFIRMED]: 3
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export function taskTokens(task) {
|
|
27
|
+
return new Set(normalizedTokens(task));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function compareLensFacts(left, right, terms) {
|
|
31
|
+
return compareNumber(isBoundary(right), isBoundary(left)) ||
|
|
32
|
+
compareNumber(termMatches(right, terms), termMatches(left, terms)) ||
|
|
33
|
+
compareNumber(STATUS_RANK[right.status] ?? 0, STATUS_RANK[left.status] ?? 0) ||
|
|
34
|
+
compareText(right.validAt, left.validAt) ||
|
|
35
|
+
compareText(left.id, right.id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function termMatches(fact, terms) {
|
|
39
|
+
const factTokens = new Set(normalizedTokens(`${fact.subject} ${fact.predicate} ${fact.object}`));
|
|
40
|
+
let matches = 0;
|
|
41
|
+
for (const term of terms) if (factTokens.has(term)) matches += 1;
|
|
42
|
+
return matches;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizedTokens(text) {
|
|
46
|
+
return String(text).normalize('NFKC').toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isBoundary(fact) {
|
|
50
|
+
return BOUNDARY_PREDICATES.includes(fact.predicate) ||
|
|
51
|
+
BOUNDARY_PREFIXES.some((prefix) => fact.predicate.startsWith(prefix));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compareNumber(left, right) {
|
|
55
|
+
return left - right;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareText(left, right) {
|
|
59
|
+
const first = String(left ?? '');
|
|
60
|
+
const second = String(right ?? '');
|
|
61
|
+
return first === second ? 0 : (first < second ? -1 : 1);
|
|
62
|
+
}
|