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.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,186 @@
1
+ import { z } from 'zod';
2
+
3
+ import { rejectDuplicateValues } from './array-validation.js';
4
+ import { createCanonicalBase64UrlSchema } from './base64url-contract.js';
5
+ import {
6
+ ProviderCapability,
7
+ WORKSPACE_PROTOCOL_VERSION,
8
+ WorkspaceRole,
9
+ WorkspaceVisibility,
10
+ } from './constants.js';
11
+ import {
12
+ idSchema,
13
+ revisionSchema,
14
+ timestampSchema,
15
+ } from './scalar-contract.js';
16
+ import {
17
+ createPreservedContentTextSchema,
18
+ createPreservedSemanticTextSchema,
19
+ } from './text-contract.js';
20
+ import { parseProtocolValue } from './validation.js';
21
+
22
+ const providerIdentifierSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{1,62}$/);
23
+ const MAX_HTTPS_URL_LENGTH = 2_048;
24
+ const ROLELESS_CAPABILITIES = new Set([
25
+ ProviderCapability.QUERY,
26
+ ProviderCapability.SYNC,
27
+ ]);
28
+
29
+ function hasSafeHttpsPath(url) {
30
+ if (/%25/iu.test(url.pathname)) {
31
+ return false;
32
+ }
33
+
34
+ let decodedPathname;
35
+
36
+ try {
37
+ decodedPathname = decodeURIComponent(url.pathname);
38
+ } catch {
39
+ return false;
40
+ }
41
+
42
+ return !/\p{C}/u.test(decodedPathname);
43
+ }
44
+
45
+ const httpsUrlSchema = z
46
+ .string()
47
+ .min(1)
48
+ .max(MAX_HTTPS_URL_LENGTH)
49
+ .transform((value, context) => {
50
+ if (
51
+ !value.startsWith('https://')
52
+ || /(?:\s|\p{C})/u.test(value)
53
+ || value.includes('?')
54
+ || value.includes('#')
55
+ || /%(?![0-9A-Fa-f]{2})/u.test(value)
56
+ ) {
57
+ context.addIssue({
58
+ code: 'custom',
59
+ message: 'Expected a canonical HTTPS URL without whitespace, query, or fragment',
60
+ });
61
+ return z.NEVER;
62
+ }
63
+
64
+ let url;
65
+
66
+ try {
67
+ url = new URL(value);
68
+ } catch {
69
+ context.addIssue({
70
+ code: 'custom',
71
+ message: 'Expected a valid HTTPS URL without credentials',
72
+ });
73
+ return z.NEVER;
74
+ }
75
+
76
+ if (
77
+ url.username !== ''
78
+ || url.password !== ''
79
+ || !hasSafeHttpsPath(url)
80
+ ) {
81
+ context.addIssue({
82
+ code: 'custom',
83
+ message: 'Expected a valid HTTPS URL without credentials',
84
+ });
85
+ return z.NEVER;
86
+ }
87
+
88
+ const canonicalUrl = url.href;
89
+
90
+ if (canonicalUrl.length > MAX_HTTPS_URL_LENGTH) {
91
+ context.addIssue({
92
+ code: 'custom',
93
+ message: 'Canonical HTTPS URL exceeds the maximum length',
94
+ });
95
+ return z.NEVER;
96
+ }
97
+
98
+ return canonicalUrl;
99
+ });
100
+
101
+ const protocolVersionsSchema = z
102
+ .array(z.literal(WORKSPACE_PROTOCOL_VERSION))
103
+ .min(1)
104
+ .max(4)
105
+ .superRefine(rejectDuplicateValues);
106
+
107
+ const capabilitiesSchema = z
108
+ .array(z.enum(Object.values(ProviderCapability)))
109
+ .min(1)
110
+ .max(8)
111
+ .superRefine(rejectDuplicateValues);
112
+
113
+ const signingKeySchema = z.strictObject({
114
+ keyId: idSchema,
115
+ algorithm: z.literal('EdDSA'),
116
+ publicJwk: z.strictObject({
117
+ kty: z.literal('OKP'),
118
+ crv: z.literal('Ed25519'),
119
+ x: createCanonicalBase64UrlSchema(32),
120
+ }),
121
+ });
122
+
123
+ const signingKeysSchema = z
124
+ .array(signingKeySchema)
125
+ .max(8)
126
+ .superRefine((keys, context) => {
127
+ rejectDuplicateValues(
128
+ keys.map(({ keyId }) => keyId),
129
+ context,
130
+ (index) => [index, 'keyId'],
131
+ );
132
+ });
133
+
134
+ export const providerManifestSchema = z.strictObject({
135
+ providerId: providerIdentifierSchema,
136
+ displayName: createPreservedSemanticTextSchema(100),
137
+ protocolVersions: protocolVersionsSchema,
138
+ apiBaseUrl: httpsUrlSchema,
139
+ webBaseUrl: httpsUrlSchema,
140
+ oidc: z.strictObject({
141
+ issuer: httpsUrlSchema,
142
+ audience: createPreservedSemanticTextSchema(200),
143
+ }),
144
+ capabilities: capabilitiesSchema,
145
+ signingKeys: signingKeysSchema,
146
+ });
147
+
148
+ export const workspaceDescriptorSchema = z
149
+ .strictObject({
150
+ id: idSchema,
151
+ slug: providerIdentifierSchema,
152
+ name: createPreservedSemanticTextSchema(120),
153
+ description: createPreservedContentTextSchema(1_000).nullable(),
154
+ visibility: z.enum(Object.values(WorkspaceVisibility)),
155
+ role: z.enum(Object.values(WorkspaceRole)).nullable(),
156
+ capabilities: capabilitiesSchema,
157
+ revision: revisionSchema,
158
+ updatedAt: timestampSchema,
159
+ })
160
+ .superRefine((workspace, context) => {
161
+ if (workspace.role !== null) {
162
+ return;
163
+ }
164
+
165
+ workspace.capabilities.forEach((capability, index) => {
166
+ if (!ROLELESS_CAPABILITIES.has(capability)) {
167
+ context.addIssue({
168
+ code: 'custom',
169
+ path: ['capabilities', index],
170
+ message: 'A roleless workspace cannot expose member capabilities',
171
+ });
172
+ }
173
+ });
174
+ });
175
+
176
+ export function parseProviderManifest(value) {
177
+ return parseProtocolValue(providerManifestSchema, value, 'Provider Manifest');
178
+ }
179
+
180
+ export function parseWorkspaceDescriptor(value) {
181
+ return parseProtocolValue(
182
+ workspaceDescriptorSchema,
183
+ value,
184
+ 'Workspace descriptor',
185
+ );
186
+ }
@@ -0,0 +1,212 @@
1
+ import { Buffer } from 'node:buffer';
2
+
3
+ import { z } from 'zod';
4
+
5
+ import { canonicalJson } from '../publication/canonical-json.js';
6
+ import { rejectDuplicateValues } from './array-validation.js';
7
+ import { createCanonicalBase64UrlSchema } from './base64url-contract.js';
8
+ import {
9
+ ProposalStatus,
10
+ WORKSPACE_PROTOCOL_VERSION,
11
+ } from './constants.js';
12
+ import { sourceReferenceSchema } from './fact-contract.js';
13
+ import { refineUtf8JsonByteBudget } from './json-budget.js';
14
+ import {
15
+ idSchema,
16
+ revisionSchema,
17
+ sourceUriSchema,
18
+ } from './scalar-contract.js';
19
+ import {
20
+ createPreservedContentTextSchema,
21
+ createPreservedSemanticTextSchema,
22
+ } from './text-contract.js';
23
+ import { parseProtocolValue } from './validation.js';
24
+
25
+ const MAX_PUBLICATION_PROPOSAL_BYTES = 1_048_576;
26
+ const PROJECTED_ZERO_SIGNATURE = Buffer.alloc(64).toString('base64url');
27
+ const changeOperationSchema = z.enum([
28
+ 'add',
29
+ 'supplement',
30
+ 'replace',
31
+ 'retract',
32
+ 'restore',
33
+ ]);
34
+ const publicationSourceSchema = sourceReferenceSchema.extend({
35
+ uri: sourceUriSchema,
36
+ });
37
+
38
+ const publicationChangeSchema = z
39
+ .strictObject({
40
+ clientFactId: idSchema,
41
+ subject: createPreservedSemanticTextSchema(500),
42
+ predicate: createPreservedSemanticTextSchema(160),
43
+ object: createPreservedContentTextSchema(20_000),
44
+ operation: changeOperationSchema,
45
+ targetFactIds: z.array(idSchema).max(50),
46
+ })
47
+ .superRefine((change, context) => {
48
+ rejectDuplicateValues(
49
+ change.targetFactIds,
50
+ context,
51
+ (index) => ['targetFactIds', index],
52
+ );
53
+
54
+ if (change.operation === 'add' && change.targetFactIds.length !== 0) {
55
+ context.addIssue({
56
+ code: 'custom',
57
+ path: ['targetFactIds'],
58
+ message: 'An add change cannot target an existing fact',
59
+ });
60
+ }
61
+
62
+ if (change.operation !== 'add' && change.targetFactIds.length === 0) {
63
+ context.addIssue({
64
+ code: 'custom',
65
+ path: ['targetFactIds'],
66
+ message: 'This change operation requires an existing target fact',
67
+ });
68
+ }
69
+ });
70
+
71
+ const unsignedPublicationProposalShape = {
72
+ protocolVersion: z.literal(WORKSPACE_PROTOCOL_VERSION),
73
+ publicationId: idSchema,
74
+ idempotencyKey: createPreservedSemanticTextSchema(200),
75
+ workspaceId: idSchema,
76
+ source: publicationSourceSchema,
77
+ changes: z.array(publicationChangeSchema).min(1).max(100),
78
+ policyVersion: createPreservedSemanticTextSchema(32),
79
+ signingKeyId: idSchema,
80
+ };
81
+
82
+ function refinePublicationProposal(proposal, context) {
83
+ const clientFactIdValues = proposal.changes.map(
84
+ ({ clientFactId }) => clientFactId,
85
+ );
86
+ const clientFactIds = new Set(clientFactIdValues);
87
+
88
+ rejectDuplicateValues(
89
+ clientFactIdValues,
90
+ context,
91
+ (index) => ['changes', index, 'clientFactId'],
92
+ );
93
+
94
+ const targetedFactIds = new Set();
95
+
96
+ proposal.changes.forEach(({ targetFactIds }, changeIndex) => {
97
+ const changeTargetFactIds = new Set();
98
+
99
+ targetFactIds.forEach((targetFactId, targetIndex) => {
100
+ const path = ['changes', changeIndex, 'targetFactIds', targetIndex];
101
+
102
+ if (clientFactIds.has(targetFactId)) {
103
+ context.addIssue({
104
+ code: 'custom',
105
+ path,
106
+ message: 'An existing target cannot match a proposal client fact ID',
107
+ });
108
+ }
109
+
110
+ if (targetedFactIds.has(targetFactId)) {
111
+ context.addIssue({
112
+ code: 'custom',
113
+ path,
114
+ message: 'An existing fact cannot be targeted by multiple changes',
115
+ });
116
+ }
117
+
118
+ changeTargetFactIds.add(targetFactId);
119
+ });
120
+
121
+ changeTargetFactIds.forEach((targetFactId) => {
122
+ targetedFactIds.add(targetFactId);
123
+ });
124
+ });
125
+
126
+ const projectedProposal = 'signature' in proposal
127
+ ? proposal
128
+ : { ...proposal, signature: PROJECTED_ZERO_SIGNATURE };
129
+
130
+ refineUtf8JsonByteBudget(
131
+ projectedProposal,
132
+ context,
133
+ MAX_PUBLICATION_PROPOSAL_BYTES,
134
+ 'Publication proposal',
135
+ );
136
+ }
137
+
138
+ const unsignedPublicationProposalSchema = z
139
+ .strictObject(unsignedPublicationProposalShape)
140
+ .superRefine(refinePublicationProposal);
141
+
142
+ export const publicationProposalSchema = z
143
+ .strictObject({
144
+ ...unsignedPublicationProposalShape,
145
+ signature: createCanonicalBase64UrlSchema(64),
146
+ })
147
+ .superRefine(refinePublicationProposal);
148
+
149
+ export const publicationResultSchema = z
150
+ .strictObject({
151
+ proposalId: idSchema,
152
+ status: z.enum(Object.values(ProposalStatus)),
153
+ workspaceRevision: revisionSchema.nullable(),
154
+ reason: createPreservedSemanticTextSchema(500).nullable(),
155
+ })
156
+ .superRefine((result, context) => {
157
+ if (
158
+ result.status === ProposalStatus.EFFECTIVE
159
+ && result.workspaceRevision === null
160
+ ) {
161
+ context.addIssue({
162
+ code: 'custom',
163
+ path: ['workspaceRevision'],
164
+ message: 'An effective proposal requires a workspace revision',
165
+ });
166
+ }
167
+
168
+ if (
169
+ result.status !== ProposalStatus.EFFECTIVE
170
+ && result.workspaceRevision !== null
171
+ ) {
172
+ context.addIssue({
173
+ code: 'custom',
174
+ path: ['workspaceRevision'],
175
+ message: 'A non-effective proposal cannot have a workspace revision',
176
+ });
177
+ }
178
+
179
+ if (result.status === ProposalStatus.REJECTED && result.reason === null) {
180
+ context.addIssue({
181
+ code: 'custom',
182
+ path: ['reason'],
183
+ message: 'A rejected proposal requires a reason',
184
+ });
185
+ }
186
+ });
187
+
188
+ export function parsePublicationProposal(value) {
189
+ return parseProtocolValue(
190
+ publicationProposalSchema,
191
+ value,
192
+ 'Publication proposal',
193
+ );
194
+ }
195
+
196
+ export function parsePublicationResult(value) {
197
+ return parseProtocolValue(
198
+ publicationResultSchema,
199
+ value,
200
+ 'Publication result',
201
+ );
202
+ }
203
+
204
+ export function publicationSigningBytes(value) {
205
+ const parsed = parseProtocolValue(
206
+ unsignedPublicationProposalSchema,
207
+ value,
208
+ 'Unsigned publication proposal',
209
+ );
210
+
211
+ return Buffer.from(canonicalJson(parsed), 'utf8');
212
+ }
@@ -0,0 +1,149 @@
1
+ import { z } from 'zod';
2
+
3
+ import { rejectDuplicateValues } from './array-validation.js';
4
+ import { factProjectionSchema } from './fact-contract.js';
5
+ import { refineUtf8JsonByteBudget } from './json-budget.js';
6
+ import {
7
+ cursorSchema,
8
+ idSchema,
9
+ revisionSchema,
10
+ timestampSchema,
11
+ } from './scalar-contract.js';
12
+ import { createNormalizedSemanticTextSchema } from './text-contract.js';
13
+ import { parseProtocolValue } from './validation.js';
14
+
15
+ const MAX_CONTEXT_PACK_BYTES = 1_048_576;
16
+
17
+ const initialWorkspaceQuerySchema = z
18
+ .strictObject({
19
+ text: createNormalizedSemanticTextSchema(512).optional(),
20
+ subjects: z.array(createNormalizedSemanticTextSchema(500)).max(20).default([]),
21
+ predicates: z.array(createNormalizedSemanticTextSchema(160)).max(20).default([]),
22
+ factIds: z.array(idSchema).max(50).default([]),
23
+ asOf: timestampSchema.optional(),
24
+ includeHistory: z.boolean().default(false),
25
+ limit: z.number().int().min(1).max(100).default(20),
26
+ cursor: z.literal(null).default(null),
27
+ })
28
+ .superRefine((query, context) => {
29
+ rejectDuplicateValues(
30
+ query.subjects,
31
+ context,
32
+ (index) => ['subjects', index],
33
+ );
34
+ rejectDuplicateValues(
35
+ query.predicates,
36
+ context,
37
+ (index) => ['predicates', index],
38
+ );
39
+ rejectDuplicateValues(
40
+ query.factIds,
41
+ context,
42
+ (index) => ['factIds', index],
43
+ );
44
+
45
+ if (
46
+ query.text === undefined
47
+ && query.subjects.length === 0
48
+ && query.predicates.length === 0
49
+ && query.factIds.length === 0
50
+ ) {
51
+ context.addIssue({
52
+ code: 'custom',
53
+ message: 'At least one query selector is required',
54
+ });
55
+ }
56
+ });
57
+
58
+ const continuationWorkspaceQuerySchema = z.strictObject({
59
+ cursor: cursorSchema,
60
+ });
61
+
62
+ export const workspaceQuerySchema = z.union([
63
+ continuationWorkspaceQuerySchema,
64
+ initialWorkspaceQuerySchema,
65
+ ]);
66
+
67
+ export const contextPackSchema = z
68
+ .strictObject({
69
+ workspaceId: idSchema,
70
+ workspaceRevision: revisionSchema,
71
+ generatedAt: timestampSchema,
72
+ freshness: z.strictObject({
73
+ state: z.enum(['fresh', 'stale']),
74
+ lastSyncedAt: timestampSchema,
75
+ }),
76
+ facts: z.array(factProjectionSchema).max(100),
77
+ nextCursor: cursorSchema.nullable(),
78
+ truncated: z.boolean(),
79
+ })
80
+ .superRefine((contextPack, context) => {
81
+ rejectDuplicateValues(
82
+ contextPack.facts.map(({ id }) => id),
83
+ context,
84
+ (index) => ['facts', index, 'id'],
85
+ );
86
+
87
+ const workspaceRevision = BigInt(contextPack.workspaceRevision);
88
+
89
+ contextPack.facts.forEach(({ revision }, index) => {
90
+ if (BigInt(revision) > workspaceRevision) {
91
+ context.addIssue({
92
+ code: 'custom',
93
+ path: ['facts', index, 'revision'],
94
+ message: 'Fact revision cannot exceed the Context Pack workspace revision',
95
+ });
96
+ }
97
+ });
98
+
99
+ if (contextPack.truncated && contextPack.nextCursor === null) {
100
+ context.addIssue({
101
+ code: 'custom',
102
+ path: ['nextCursor'],
103
+ message: 'A truncated Context Pack requires a next cursor',
104
+ });
105
+ }
106
+
107
+ if (!contextPack.truncated && contextPack.nextCursor !== null) {
108
+ context.addIssue({
109
+ code: 'custom',
110
+ path: ['nextCursor'],
111
+ message: 'A complete Context Pack cannot have a next cursor',
112
+ });
113
+ }
114
+
115
+ const generatedAtEpoch = Date.parse(contextPack.generatedAt);
116
+
117
+ contextPack.facts.forEach(({ recordedAt }, index) => {
118
+ if (Date.parse(recordedAt) > generatedAtEpoch) {
119
+ context.addIssue({
120
+ code: 'custom',
121
+ path: ['facts', index, 'recordedAt'],
122
+ message: 'Fact recording time cannot follow Context Pack generation',
123
+ });
124
+ }
125
+ });
126
+
127
+ if (Date.parse(contextPack.freshness.lastSyncedAt) > generatedAtEpoch) {
128
+ context.addIssue({
129
+ code: 'custom',
130
+ path: ['freshness', 'lastSyncedAt'],
131
+ message: 'Sync time cannot follow Context Pack generation',
132
+ });
133
+ }
134
+
135
+ refineUtf8JsonByteBudget(
136
+ contextPack,
137
+ context,
138
+ MAX_CONTEXT_PACK_BYTES,
139
+ 'Context Pack',
140
+ );
141
+ });
142
+
143
+ export function parseWorkspaceQuery(value) {
144
+ return parseProtocolValue(workspaceQuerySchema, value, 'Workspace query');
145
+ }
146
+
147
+ export function parseContextPack(value) {
148
+ return parseProtocolValue(contextPackSchema, value, 'Context Pack');
149
+ }