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.
Files changed (206) hide show
  1. package/README.en.md +2 -333
  2. package/README.md +370 -202
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -5
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +51 -28
  59. package/src/cli/local-runtime-options.js +14 -8
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +1 -1
  92. package/src/local-runtime/lifecycle.js +26 -15
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server.js +36 -17
  100. package/src/setup/agent-installation-status.js +2 -1
  101. package/src/setup/agents.js +2 -1
  102. package/src/setup/claude-code-config.js +2 -1
  103. package/src/setup/codex-bootstrap.js +12 -7
  104. package/src/setup/container-runtime.js +25 -21
  105. package/src/setup/graph-runtime.js +148 -56
  106. package/src/setup/node-runtime.js +3 -3
  107. package/src/setup/options.js +2 -4
  108. package/src/setup/paths.js +18 -3
  109. package/src/setup/review-skill-adapters.js +49 -0
  110. package/src/setup/setup.js +26 -2
  111. package/src/setup/uninstall.js +4 -2
  112. package/src/system/resource-monitor.js +317 -0
  113. package/src/system/runtime-settings.js +131 -0
  114. package/src/system/system-service.js +119 -0
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. package/src/store.js +0 -2
@@ -1,103 +0,0 @@
1
- import { SpaceKind } from '../models.js';
2
- import {
3
- isSafeResourceFact,
4
- isSafeResourceText,
5
- projectCurrentFact,
6
- projectHistoryItem,
7
- projectSubscription
8
- } from './lens-resource-projection.js';
9
- import { selectCurrentFacts, selectHistoryItems } from './lens-resource-selection.js';
10
-
11
- export const CURRENT_LENS_BUDGET_BYTES = 16 * 1024;
12
- export const HISTORY_BUDGET_BYTES = 64 * 1024;
13
- export const HISTORY_LIMIT = 100;
14
-
15
- export class LensResourceService {
16
- constructor({ store, lens, activePersonalSpace }) {
17
- this.store = store;
18
- this.lens = lens;
19
- this.activePersonalSpace = activePersonalSpace;
20
- }
21
-
22
- current() {
23
- const personal = this.activePersonalSpace();
24
- const result = this.lens.getUserLens({
25
- personalSpaceId: personal.id,
26
- task: '',
27
- budget: CURRENT_LENS_BUDGET_BYTES,
28
- includeObserved: true,
29
- includeSuggested: false,
30
- includeRestricted: false
31
- });
32
- const projected = result.facts.map(projectCurrentFact).filter(Boolean);
33
- const selected = selectCurrentFacts(projected, CURRENT_LENS_BUDGET_BYTES);
34
- return safePayload({
35
- personalSpaceId: personal.id,
36
- facts: selected.facts,
37
- text: selected.text,
38
- truncated: result.truncated ||
39
- projected.length < result.facts.length ||
40
- selected.truncated,
41
- budget: {
42
- encoding: 'utf-8',
43
- limitBytes: CURRENT_LENS_BUDGET_BYTES,
44
- usedBytes: Buffer.byteLength(selected.text, 'utf8')
45
- }
46
- });
47
- }
48
-
49
- history() {
50
- const personal = this.activePersonalSpace();
51
- const result = this.lens.searchUserContext({
52
- personalSpaceId: personal.id,
53
- query: '',
54
- includeHistorical: true,
55
- includeRestricted: false,
56
- maxFactObjectBytes: HISTORY_BUDGET_BYTES,
57
- maxEvidenceBodyBytes: 0,
58
- scanLimit: HISTORY_LIMIT * 2,
59
- factFilter: isSafeResourceFact,
60
- limit: HISTORY_LIMIT
61
- });
62
- const safeItems = result.facts.map(projectHistoryItem).filter(Boolean);
63
- const selected = selectHistoryItems(safeItems, {
64
- limit: HISTORY_LIMIT,
65
- budgetBytes: HISTORY_BUDGET_BYTES
66
- });
67
- return safePayload({
68
- personalSpaceId: personal.id,
69
- limit: HISTORY_LIMIT,
70
- count: selected.items.length,
71
- truncated: result.truncated ||
72
- safeItems.length < result.facts.length ||
73
- selected.truncated,
74
- budget: {
75
- encoding: 'utf-8',
76
- limitBytes: HISTORY_BUDGET_BYTES,
77
- itemsBytes: selected.itemsBytes
78
- },
79
- items: selected.items
80
- }, HISTORY_BUDGET_BYTES);
81
- }
82
-
83
- subscribed() {
84
- const personal = this.activePersonalSpace();
85
- const subscriptions = this.store.subscriptionsFor(personal.id)
86
- .map((subscription) => ({ subscription, space: this.store.getSpace(subscription.spaceId) }))
87
- .filter(({ space }) => space?.kind === SpaceKind.PUBLIC)
88
- .map(({ subscription, space }) => projectSubscription(subscription, space))
89
- .filter(Boolean);
90
- return safePayload({ personalSpaceId: personal.id, subscriptions });
91
- }
92
- }
93
-
94
- function safePayload(payload, limitBytes = null) {
95
- const serialized = JSON.stringify(payload);
96
- if (!isSafeResourceText(serialized)) {
97
- throw new Error('Unsafe resource projection');
98
- }
99
- if (limitBytes && Buffer.byteLength(serialized, 'utf8') > limitBytes) {
100
- throw new Error('Resource projection exceeds its byte budget');
101
- }
102
- return payload;
103
- }
@@ -1,122 +0,0 @@
1
- import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
- import { FactScope } from '../models.js';
3
- import { MAX_EPISODE_EVIDENCE_BODY_BYTES } from '../storage/episode-evidence-preview.js';
4
- import { assertStorePort } from '../storage/store-port.js';
5
- import { requirePersonalSpace } from './lens-input.js';
6
- import {
7
- buildCorrectionIndex,
8
- isSearchableFact,
9
- projectSearchFact
10
- } from './lens-query-projection.js';
11
-
12
- export const DEFAULT_SEARCH_USER_CONTEXT_LIMIT = 20;
13
- export const MAX_SEARCH_USER_CONTEXT_LIMIT = 100;
14
- const MAX_SEARCH_USER_CONTEXT_SCAN_LIMIT = 200;
15
-
16
- export class LensSearchQuery {
17
- constructor(store) {
18
- assertStorePort(store);
19
- this.store = store;
20
- }
21
-
22
- searchUserContext({
23
- personalSpaceId,
24
- query,
25
- includeHistorical = false,
26
- includeRestricted = false,
27
- requireSafeSource = false,
28
- maxFactObjectBytes = null,
29
- maxEvidenceBodyBytes = MAX_EPISODE_EVIDENCE_BODY_BYTES,
30
- limit = DEFAULT_SEARCH_USER_CONTEXT_LIMIT,
31
- scanLimit = limit,
32
- factFilter = null
33
- }) {
34
- requirePersonalSpace(this.store, personalSpaceId);
35
- validateQuery(query);
36
- validateSearchLimit(limit);
37
- validateScanOptions(scanLimit, limit, factFilter);
38
-
39
- const allowRestricted = includeRestricted === true;
40
- const page = this.store.searchFactsPage([personalSpaceId], query, {
41
- includeHistorical,
42
- scope: FactScope.PERSONAL,
43
- excludeRestricted: !allowRestricted,
44
- safeSourceSpaceId: requireSafeSource === true ? personalSpaceId : null,
45
- maxObjectBytes: maxFactObjectBytes,
46
- limit: scanLimit + 1
47
- });
48
- const candidates = page.facts;
49
- const scanned = candidates.slice(0, scanLimit);
50
- const facts = scanned
51
- .filter((fact) => fact.spaceId === personalSpaceId && fact.scope === FactScope.PERSONAL)
52
- .filter((fact) => isSearchableFact(fact, {
53
- includeHistorical,
54
- includeRestricted: allowRestricted
55
- }))
56
- .filter((fact) => factFilter?.(fact) ?? true);
57
- const selected = facts.slice(0, limit);
58
- const correctionIndex = buildCorrectionIndex(
59
- this.store.correctionEpisodeEvidencePreviews(
60
- personalSpaceId,
61
- selected.map((fact) => fact.id),
62
- { includeRestricted: allowRestricted, maxBodyBytes: maxEvidenceBodyBytes }
63
- ),
64
- personalSpaceId,
65
- allowRestricted
66
- );
67
- return {
68
- personalSpaceId,
69
- query,
70
- truncated: page.objectLimitTruncated ||
71
- candidates.length > scanLimit ||
72
- facts.length > limit ||
73
- facts.length < scanned.length,
74
- facts: selected.map((fact) => {
75
- const source = this.store.episodeEvidencePreview(
76
- personalSpaceId,
77
- fact.sourceEpisodeId,
78
- { includeRestricted: allowRestricted, maxBodyBytes: maxEvidenceBodyBytes }
79
- );
80
- return projectSearchFact(
81
- this.store,
82
- fact,
83
- source,
84
- personalSpaceId,
85
- correctionIndex,
86
- allowRestricted
87
- );
88
- })
89
- };
90
- }
91
- }
92
-
93
- function validateScanOptions(scanLimit, limit, factFilter) {
94
- if (
95
- !Number.isInteger(scanLimit) ||
96
- scanLimit < limit ||
97
- scanLimit > MAX_SEARCH_USER_CONTEXT_SCAN_LIMIT
98
- ) {
99
- throw new ApplicationError(
100
- ApplicationErrorCode.VALIDATION,
101
- `Scan limit must be an integer between the result limit and ${MAX_SEARCH_USER_CONTEXT_SCAN_LIMIT}`
102
- );
103
- }
104
- if (factFilter !== null && typeof factFilter !== 'function') {
105
- throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Fact filter must be a function');
106
- }
107
- }
108
-
109
- function validateQuery(query) {
110
- if (typeof query !== 'string') {
111
- throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Query must be a string');
112
- }
113
- }
114
-
115
- function validateSearchLimit(limit) {
116
- if (!Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_USER_CONTEXT_LIMIT) {
117
- throw new ApplicationError(
118
- ApplicationErrorCode.VALIDATION,
119
- `Limit must be an integer between 1 and ${MAX_SEARCH_USER_CONTEXT_LIMIT}`
120
- );
121
- }
122
- }
@@ -1,183 +0,0 @@
1
- import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
2
- import { FactScope, FactStatus, Sensitivity } from '../models.js';
3
- import { assertSafeSourceMetadata } from '../security/source-metadata-policy.js';
4
- import { assertStorePort } from '../storage/store-port.js';
5
- import { writePersonalFact, writePersonalFactSpecs } from './lens-fact-writer.js';
6
- import { assertSafeLensTexts, requirePersonalSpace, validateLensWriteInput } from './lens-input.js';
7
- import { LensCorrectionService } from './lens-correction-service.js';
8
- const OBSERVATION_INFERENCES = new Set(['direct', 'inferred']);
9
- const IMMEDIATE = { mode: 'immediate' };
10
- const SOURCE_METADATA_ERROR = 'Sensitive source metadata is not allowed in Personal Lens';
11
-
12
- export class LensService {
13
- #corrections;
14
-
15
- constructor(store) {
16
- assertStorePort(store);
17
- this.store = store;
18
- this.#corrections = new LensCorrectionService(store);
19
- }
20
- rememberUserFact({
21
- personalSpaceId,
22
- subject = 'user',
23
- predicate,
24
- value,
25
- sourceText,
26
- sourceKind = 'conversation',
27
- sensitivity = Sensitivity.NORMAL,
28
- confidence = 1
29
- }) {
30
- assertSafeSourceMetadata(sourceKind, null, SOURCE_METADATA_ERROR);
31
- this.#validate(personalSpaceId, [subject, predicate, value, sourceText], sensitivity, confidence);
32
- return this.store.transaction(() => {
33
- const episode = this.store.addEpisode(personalSpaceId, sourceKind, sourceText);
34
- const fact = this.#writeFact({
35
- personalSpaceId, subject, predicate, value, episode,
36
- status: FactStatus.CONFIRMED, sensitivity, confidence
37
- });
38
- return { episode, fact };
39
- }, IMMEDIATE);
40
- }
41
- rememberUserFactsFromSourceInCurrentTransaction({
42
- personalSpaceId,
43
- subject = 'user',
44
- specs,
45
- sourceText,
46
- sourceKind,
47
- sourceUri = null,
48
- sensitivity = Sensitivity.NORMAL,
49
- confidence = 1
50
- }) {
51
- assertSafeSourceMetadata(sourceKind, sourceUri, SOURCE_METADATA_ERROR);
52
- const specTexts = specs.flatMap(specTextsForValidation);
53
- this.#validate(
54
- personalSpaceId,
55
- [subject, sourceText, ...specTexts],
56
- sensitivity,
57
- confidence
58
- );
59
- const episode = this.store.addEpisode(personalSpaceId, sourceKind, sourceText, sourceUri);
60
- const facts = writePersonalFactSpecs(this.store, {
61
- spaceId: personalSpaceId,
62
- subject,
63
- specs,
64
- sourceEpisodeId: episode.id,
65
- sensitivity,
66
- confidence
67
- });
68
- return { episode, facts };
69
- }
70
- writeUserFactsForEpisodeInCurrentTransaction({
71
- personalSpaceId,
72
- subject = 'user',
73
- specs,
74
- episode,
75
- sensitivity = Sensitivity.NORMAL,
76
- confidence = 1
77
- }) {
78
- assertSafeSourceMetadata(episode?.sourceKind, episode?.sourceUri, SOURCE_METADATA_ERROR);
79
- if (episode.spaceId !== personalSpaceId) {
80
- throw new ApplicationError(ApplicationErrorCode.VALIDATION, 'Lens source episode belongs to another space');
81
- }
82
- this.#validate(
83
- personalSpaceId,
84
- [subject, episode.body, ...specs.flatMap(specTextsForValidation)],
85
- sensitivity,
86
- confidence
87
- );
88
- const facts = writePersonalFactSpecs(this.store, {
89
- spaceId: personalSpaceId,
90
- subject,
91
- specs,
92
- sourceEpisodeId: episode.id,
93
- sensitivity,
94
- confidence
95
- });
96
- return { episode, facts };
97
- }
98
- submitUserObservation({
99
- personalSpaceId,
100
- subject = 'user',
101
- predicate,
102
- value,
103
- evidenceText,
104
- inference,
105
- sourceKind = 'observation',
106
- sensitivity = Sensitivity.NORMAL,
107
- confidence = inference === 'inferred' ? 0.5 : 1
108
- }) {
109
- assertSafeSourceMetadata(sourceKind, null, SOURCE_METADATA_ERROR);
110
- this.#validate(personalSpaceId, [subject, predicate, value, evidenceText], sensitivity, confidence);
111
- if (!OBSERVATION_INFERENCES.has(inference)) {
112
- throw new ApplicationError(
113
- ApplicationErrorCode.VALIDATION,
114
- 'Observation inference must be direct or inferred'
115
- );
116
- }
117
- return this.store.transaction(() => {
118
- const episode = this.store.addEpisode(personalSpaceId, sourceKind, evidenceText);
119
- const fact = this.#writeFact({
120
- personalSpaceId, subject, predicate, value, episode,
121
- status: inference === 'inferred' ? FactStatus.SUGGESTED : FactStatus.OBSERVED,
122
- sensitivity, confidence
123
- });
124
- return { episode, fact };
125
- }, IMMEDIATE);
126
- }
127
- confirmObservation({ personalSpaceId, factId, sourceText, sourceKind = 'confirmation' }) {
128
- assertSafeSourceMetadata(sourceKind, null, SOURCE_METADATA_ERROR);
129
- requirePersonalSpace(this.store, personalSpaceId);
130
- assertSafeLensTexts([sourceText]);
131
- return this.store.transaction(() => {
132
- const original = this.#findConfirmableFact(personalSpaceId, factId);
133
- assertSafeLensTexts([original.subject, original.predicate, original.object]);
134
- const episode = this.store.addEpisode(personalSpaceId, sourceKind, sourceText);
135
- const fact = this.#writeFact({
136
- personalSpaceId,
137
- subject: original.subject,
138
- predicate: original.predicate,
139
- value: original.object,
140
- episode,
141
- status: FactStatus.CONFIRMED,
142
- sensitivity: original.sensitivity,
143
- confidence: original.confidence
144
- });
145
- return { episode, fact, replacedFact: original };
146
- }, IMMEDIATE);
147
- }
148
- correctUserFact(input) {
149
- assertSafeSourceMetadata(input.sourceKind, null, SOURCE_METADATA_ERROR);
150
- return this.#corrections.correctUserFact(input);
151
- }
152
-
153
- #writeFact(input) {
154
- return writePersonalFact(this.store, {
155
- spaceId: input.personalSpaceId,
156
- subject: input.subject,
157
- predicate: input.predicate,
158
- object: input.value,
159
- sourceEpisodeId: input.episode.id,
160
- status: input.status,
161
- sensitivity: input.sensitivity,
162
- confidence: input.confidence
163
- });
164
- }
165
- #validate(personalSpaceId, texts, sensitivity, confidence) {
166
- validateLensWriteInput(this.store, { personalSpaceId, texts, sensitivity, confidence });
167
- }
168
-
169
- #findConfirmableFact(personalSpaceId, factId) {
170
- const fact = this.store.getFact(factId);
171
- if (!fact || fact.spaceId !== personalSpaceId || fact.scope !== FactScope.PERSONAL) {
172
- throw new ApplicationError(ApplicationErrorCode.NOT_FOUND, `Observation not found: ${factId}`);
173
- }
174
- if (fact.invalidAt || ![FactStatus.OBSERVED, FactStatus.SUGGESTED].includes(fact.status)) {
175
- throw new ApplicationError(ApplicationErrorCode.VALIDATION, `Observation cannot be confirmed: ${factId}`);
176
- }
177
- return fact;
178
- }
179
- }
180
- function specTextsForValidation(spec) {
181
- return [spec.subject, spec.predicate, spec.object, spec.oldValue, spec.newValue]
182
- .filter(value => value !== undefined);
183
- }
package/src/models.js DELETED
@@ -1,44 +0,0 @@
1
- export const SpaceKind = Object.freeze({
2
- PERSONAL: 'personal',
3
- PUBLIC: 'public'
4
- });
5
-
6
- export const CandidateStatus = Object.freeze({
7
- PENDING: 'pending',
8
- SYNCED: 'synced',
9
- PERSONAL_ONLY: 'personal_only',
10
- IGNORED: 'ignored'
11
- });
12
-
13
- export const FactStatus = Object.freeze({
14
- CONFIRMED: 'confirmed',
15
- OBSERVED: 'observed',
16
- SUGGESTED: 'suggested',
17
- REJECTED: 'rejected',
18
- DEPRECATED: 'deprecated'
19
- });
20
-
21
- export const Sensitivity = Object.freeze({
22
- NORMAL: 'normal',
23
- PRIVATE: 'private',
24
- RESTRICTED: 'restricted'
25
- });
26
-
27
- export const FactScope = Object.freeze({
28
- PERSONAL: 'personal',
29
- PUBLIC: 'public'
30
- });
31
-
32
- export const PublishRoute = Object.freeze({
33
- PERSONAL: 'personal',
34
- PUBLIC: 'public',
35
- CANDIDATE: 'candidate'
36
- });
37
-
38
- export function nowIso() {
39
- return new Date().toISOString();
40
- }
41
-
42
- export function isCurrentFact(fact) {
43
- return !fact.invalidAt && ![FactStatus.REJECTED, FactStatus.DEPRECATED].includes(fact.status);
44
- }
package/src/observer.js DELETED
@@ -1,83 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
-
3
- import { IngestionService } from './ingestion.js';
4
-
5
- const CONTEXT_MARKERS = ['可能', '也许', '不确定', '待确认', '以后要', '似乎'];
6
- const URL_PATTERN = /https?:\/\//;
7
- const KEY_VALUE_PATTERN = /^[\p{L}\p{N}_.-]+\s*[::]\s*\S/u;
8
- const FORBIDDEN_RULE_PATTERN =
9
- /^(?:这个项目|本项目|项目)?\s*(?:禁止|不要|不允许)\s*(?:使用|采用|引入|用)?\s*\S+/;
10
-
11
- export function observeGitDiff({ store, personalSpaceId, targetSpaceId = null, cwd = process.cwd() }) {
12
- const service = new IngestionService(store);
13
- const resolvedTargetSpaceId = targetSpaceId ?? inferOnlySubscribedSpace(store, personalSpaceId);
14
- const observed = collectGitDiffEpisodes({ cwd }).map((episode) => ({
15
- ...episode,
16
- result: service.remember({
17
- personalSpaceId,
18
- targetSpaceId: resolvedTargetSpaceId,
19
- sourceKind: 'git',
20
- body: episode.body,
21
- sourceUri: episode.sourceUri
22
- })
23
- }));
24
-
25
- return { observed };
26
- }
27
-
28
- function inferOnlySubscribedSpace(store, personalSpaceId) {
29
- const subscriptions = store.subscriptionsFor(personalSpaceId);
30
- return subscriptions.length === 1 ? subscriptions[0].spaceId : null;
31
- }
32
-
33
- export function collectGitDiffEpisodes({ cwd = process.cwd(), maxBuffer = 1024 * 1024 } = {}) {
34
- const diff = execFileSync('git', ['diff', '--unified=0', '--no-ext-diff'], {
35
- cwd,
36
- encoding: 'utf8',
37
- maxBuffer
38
- });
39
- return parseGitDiff(diff);
40
- }
41
-
42
- export function parseGitDiff(diff) {
43
- const episodes = [];
44
- let currentPath = null;
45
-
46
- for (const rawLine of diff.split(/\r?\n/)) {
47
- if (rawLine.startsWith('+++ b/')) {
48
- currentPath = rawLine.slice('+++ b/'.length);
49
- continue;
50
- }
51
-
52
- if (rawLine.startsWith('+++ ')) {
53
- currentPath = null;
54
- continue;
55
- }
56
-
57
- if (!rawLine.startsWith('+')) continue;
58
-
59
- const body = rawLine.slice(1).trim();
60
- if (!body) continue;
61
- if (!isContextWorthyLine(body)) continue;
62
-
63
- episodes.push({
64
- body,
65
- sourceUri: `git-diff:${currentPath ?? 'unknown'}`
66
- });
67
- }
68
-
69
- return episodes;
70
- }
71
-
72
- function isContextWorthyLine(body) {
73
- return (
74
- KEY_VALUE_PATTERN.test(body) ||
75
- URL_PATTERN.test(body) ||
76
- body.startsWith('替代:') ||
77
- body.startsWith('替代:') ||
78
- body.startsWith('禁止:') ||
79
- body.startsWith('禁止:') ||
80
- FORBIDDEN_RULE_PATTERN.test(body) ||
81
- CONTEXT_MARKERS.some((marker) => body.includes(marker))
82
- );
83
- }
@@ -1,33 +0,0 @@
1
- export function getProjectRules(store, spaceId) {
2
- const facts = store.currentFacts(spaceId).map((fact) => enrichFact(store, fact));
3
-
4
- return {
5
- spaceId,
6
- spaceName: store.getSpace(spaceId)?.name ?? spaceId,
7
- forbidden: facts.filter((fact) => fact.predicate === 'forbids'),
8
- parameters: facts.filter(isProjectParameter),
9
- links: facts.filter((fact) => fact.predicate === 'has_url'),
10
- facts
11
- };
12
- }
13
-
14
- function enrichFact(store, fact) {
15
- const episode = store.getEpisode(fact.sourceEpisodeId);
16
-
17
- return {
18
- ...fact,
19
- source: episode
20
- ? {
21
- id: episode.id,
22
- kind: episode.sourceKind,
23
- uri: episode.sourceUri,
24
- body: episode.body,
25
- createdAt: episode.createdAt
26
- }
27
- : null
28
- };
29
- }
30
-
31
- function isProjectParameter(fact) {
32
- return fact.predicate.startsWith('has_') && fact.predicate !== 'has_url';
33
- }
@@ -1,46 +0,0 @@
1
- const MAX_LOOKAHEAD = '9999-12-31T23:59:59.999Z';
2
- const SAFE_FAILURE = 'Publication delivery failed';
3
-
4
- export class OutboxService {
5
- constructor(store, { now = () => new Date() } = {}) {
6
- this.store = store;
7
- this.now = now;
8
- }
9
-
10
- pending(at = this.now()) {
11
- return this.store.listPendingOutbox(toIso(at));
12
- }
13
-
14
- markSent(id) {
15
- return this.store.transaction(() => {
16
- this.#requirePending(id);
17
- return this.store.markOutboxSent(id, toIso(this.now()));
18
- }, { mode: 'immediate' });
19
- }
20
-
21
- markFailed(id, _error) {
22
- return this.store.transaction(() => {
23
- const row = this.#requirePending(id);
24
- const now = new Date(this.now());
25
- const delayMinutes = retryDelayMinutes(row.attempts + 1);
26
- const nextAttemptAt = new Date(now.getTime() + delayMinutes * 60_000).toISOString();
27
- return this.store.markOutboxFailed(id, SAFE_FAILURE, nextAttemptAt);
28
- }, { mode: 'immediate' });
29
- }
30
-
31
- #requirePending(id) {
32
- const row = this.store.listPendingOutbox(MAX_LOOKAHEAD).find((item) => item.id === id);
33
- if (!row) throw new Error(`Pending Outbox row not found: ${id}`);
34
- return row;
35
- }
36
- }
37
-
38
- export function retryDelayMinutes(attempt) {
39
- return Math.min(30, 2 ** Math.max(0, attempt - 1));
40
- }
41
-
42
- function toIso(value) {
43
- const date = value instanceof Date ? value : new Date(value);
44
- if (Number.isNaN(date.getTime())) throw new TypeError('Outbox time must be valid');
45
- return date.toISOString();
46
- }
@@ -1,75 +0,0 @@
1
- const ENVELOPE_KEYS = ['contentHash', 'facts', 'id', 'policyVersion', 'source', 'spaceId'];
2
- const SOURCE_KEYS = ['capturedAt', 'episodeId', 'kind', 'uri'];
3
- const FACT_KEYS = ['id', 'object', 'predicate', 'sourceEpisodeId', 'subject'];
4
-
5
- export function validateEnvelopeSchema(envelope) {
6
- assertExactObject('Publication envelope', envelope, ENVELOPE_KEYS);
7
- assertNonEmptyString('Publication envelope id', envelope.id);
8
- assertNonEmptyString('Publication envelope spaceId', envelope.spaceId);
9
- if (envelope.policyVersion !== '1') {
10
- throw new TypeError('Publication envelope policyVersion must be 1');
11
- }
12
- if (!/^[a-f0-9]{64}$/.test(envelope.contentHash)) {
13
- throw new TypeError('Publication envelope contentHash must be lowercase SHA-256');
14
- }
15
-
16
- validateSource(envelope.source);
17
- validateFacts(envelope.facts, envelope.source.episodeId);
18
- return envelope;
19
- }
20
-
21
- function validateSource(source) {
22
- assertExactObject('Publication source', source, SOURCE_KEYS);
23
- assertNonEmptyString('Publication source episodeId', source.episodeId);
24
- assertNonEmptyString('Publication source kind', source.kind);
25
- if (source.uri !== null && typeof source.uri !== 'string') {
26
- throw new TypeError('Publication source uri must be a string or null');
27
- }
28
- assertNonEmptyString('Publication source capturedAt', source.capturedAt);
29
- const capturedAt = new Date(source.capturedAt);
30
- if (Number.isNaN(capturedAt.getTime()) || capturedAt.toISOString() !== source.capturedAt) {
31
- throw new TypeError('Publication source capturedAt must be an ISO timestamp');
32
- }
33
- }
34
-
35
- function validateFacts(facts, episodeId) {
36
- if (!Array.isArray(facts) || facts.length === 0) {
37
- throw new TypeError('Publication envelope facts must be a non-empty array');
38
- }
39
- const ids = new Set();
40
- for (const fact of facts) {
41
- assertExactObject('Publication fact', fact, FACT_KEYS);
42
- assertNonEmptyString('Publication fact id', fact.id);
43
- assertNonEmptyString('Publication fact subject', fact.subject);
44
- assertNonEmptyString('Publication fact predicate', fact.predicate);
45
- if (typeof fact.object !== 'string') {
46
- throw new TypeError('Publication fact object must be a string');
47
- }
48
- assertNonEmptyString('Publication fact sourceEpisodeId', fact.sourceEpisodeId);
49
- if (fact.sourceEpisodeId !== episodeId) {
50
- throw new TypeError('Publication fact sourceEpisodeId must match the source episode');
51
- }
52
- if (ids.has(fact.id)) throw new TypeError(`Duplicate publication fact id: ${fact.id}`);
53
- ids.add(fact.id);
54
- }
55
- }
56
-
57
- function assertExactObject(label, value, expectedKeys) {
58
- if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object`);
59
- const keys = Object.keys(value).sort();
60
- if (keys.length !== expectedKeys.length || keys.some((key, index) => key !== expectedKeys[index])) {
61
- throw new TypeError(`${label} has invalid schema keys`);
62
- }
63
- }
64
-
65
- function assertNonEmptyString(label, value) {
66
- if (typeof value !== 'string' || value.length === 0) {
67
- throw new TypeError(`${label} must be a non-empty string`);
68
- }
69
- }
70
-
71
- function isPlainObject(value) {
72
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
73
- const prototype = Object.getPrototypeOf(value);
74
- return prototype === Object.prototype || prototype === null;
75
- }