@retinue/agentkit 0.2.0 → 0.3.1

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 (209) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/memory/connections.d.ts +10 -0
  6. package/dist/adapters/memory/connections.js +87 -0
  7. package/dist/adapters/memory/graph.d.ts +17 -0
  8. package/dist/adapters/memory/graph.js +267 -0
  9. package/dist/adapters/memory/index.d.ts +3 -0
  10. package/dist/adapters/memory/index.js +3 -0
  11. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  12. package/dist/adapters/memory/rate-limit.js +28 -0
  13. package/dist/adapters/memory/runtime.js +0 -0
  14. package/dist/adapters/postgres/connections.d.ts +15 -0
  15. package/dist/adapters/postgres/connections.js +134 -0
  16. package/dist/adapters/postgres/graph.d.ts +24 -0
  17. package/dist/adapters/postgres/graph.js +325 -0
  18. package/dist/adapters/postgres/index.d.ts +2 -0
  19. package/dist/adapters/postgres/index.js +2 -0
  20. package/dist/adapters/postgres/migrations.js +198 -0
  21. package/dist/adapters/postgres/run-store.js +26 -2
  22. package/dist/adapters/postgres/schema.d.ts +61 -2
  23. package/dist/adapters/postgres/schema.js +95 -10
  24. package/dist/adapters/redis/index.d.ts +1 -0
  25. package/dist/adapters/redis/index.js +1 -0
  26. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  27. package/dist/adapters/redis/rate-limit.js +55 -0
  28. package/dist/adapters/supabase/index.d.ts +15 -0
  29. package/dist/adapters/supabase/index.js +15 -0
  30. package/dist/adapters/supabase/rls.js +33 -0
  31. package/dist/agents/agent.d.ts +78 -1
  32. package/dist/agents/agent.js +118 -5
  33. package/dist/agents/define.js +24 -4
  34. package/dist/agents/engine.d.ts +20 -0
  35. package/dist/agents/engine.js +177 -6
  36. package/dist/audio/index.d.ts +151 -0
  37. package/dist/audio/index.js +183 -0
  38. package/dist/connections/cipher.d.ts +103 -0
  39. package/dist/connections/cipher.js +141 -0
  40. package/dist/connections/index.d.ts +156 -0
  41. package/dist/connections/index.js +72 -0
  42. package/dist/connections/oauth/client.d.ts +84 -0
  43. package/dist/connections/oauth/client.js +141 -0
  44. package/dist/connections/oauth/index.d.ts +166 -0
  45. package/dist/connections/oauth/index.js +226 -0
  46. package/dist/connections/oauth/service.d.ts +90 -0
  47. package/dist/connections/oauth/service.js +132 -0
  48. package/dist/connections/pause.d.ts +68 -0
  49. package/dist/connections/pause.js +97 -0
  50. package/dist/connections/resolver.d.ts +59 -0
  51. package/dist/connections/resolver.js +138 -0
  52. package/dist/connections/resume.d.ts +64 -0
  53. package/dist/connections/resume.js +99 -0
  54. package/dist/connections/vault-cipher.d.ts +95 -0
  55. package/dist/connections/vault-cipher.js +206 -0
  56. package/dist/core/content-parts.d.ts +18 -2
  57. package/dist/core/content-parts.js +1 -0
  58. package/dist/core/context.d.ts +25 -0
  59. package/dist/core/errors.d.ts +1 -1
  60. package/dist/core/errors.js +15 -0
  61. package/dist/core/events.d.ts +44 -2
  62. package/dist/core/events.js +31 -2
  63. package/dist/core/validation.js +19 -0
  64. package/dist/entries/connections.d.ts +10 -0
  65. package/dist/entries/connections.js +10 -0
  66. package/dist/entries/knowledge.d.ts +8 -0
  67. package/dist/entries/knowledge.js +8 -0
  68. package/dist/entries/mcp-server.d.ts +9 -0
  69. package/dist/entries/mcp-server.js +9 -0
  70. package/dist/entries/runtime.d.ts +10 -0
  71. package/dist/entries/runtime.js +8 -0
  72. package/dist/entries/testing.d.ts +28 -0
  73. package/dist/entries/testing.js +28 -0
  74. package/dist/graphql/resolvers.d.ts +3 -1
  75. package/dist/graphql/resolvers.js +14 -0
  76. package/dist/knowledge/communities.d.ts +166 -0
  77. package/dist/knowledge/communities.js +377 -0
  78. package/dist/knowledge/graph-global.d.ts +153 -0
  79. package/dist/knowledge/graph-global.js +246 -0
  80. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  81. package/dist/knowledge/graph-retrieval.js +303 -0
  82. package/dist/knowledge/graph.d.ts +223 -0
  83. package/dist/knowledge/graph.js +369 -0
  84. package/dist/knowledge/index.d.ts +24 -0
  85. package/dist/knowledge/index.js +36 -1
  86. package/dist/knowledge/retrieval.d.ts +26 -1
  87. package/dist/knowledge/retrieval.js +77 -1
  88. package/dist/mcp-server/index.d.ts +168 -0
  89. package/dist/mcp-server/index.js +175 -0
  90. package/dist/models/extraction.d.ts +73 -0
  91. package/dist/models/extraction.js +118 -0
  92. package/dist/models/index.d.ts +40 -1
  93. package/dist/models/index.js +22 -1
  94. package/dist/models/pricing.d.ts +2 -0
  95. package/dist/models/pricing.js +17 -1
  96. package/dist/models/provider-factory.js +9 -7
  97. package/dist/models/streaming.d.ts +89 -0
  98. package/dist/models/streaming.js +179 -2
  99. package/dist/persistence/index.d.ts +230 -0
  100. package/dist/persistence/index.js +2 -0
  101. package/dist/runtime/index.d.ts +1 -1
  102. package/dist/runtime/index.js +12 -0
  103. package/dist/runtime/worker.d.ts +36 -1
  104. package/dist/runtime/worker.js +37 -4
  105. package/dist/security/checklist.js +46 -0
  106. package/dist/server/bin.d.ts +72 -0
  107. package/dist/server/bin.js +304 -0
  108. package/dist/server/boot.d.ts +20 -2
  109. package/dist/server/boot.js +29 -3
  110. package/dist/server/cli-worker.js +6 -3
  111. package/dist/server/cli.d.ts +13 -0
  112. package/dist/server/cli.js +10 -5
  113. package/dist/server/config.d.ts +11 -1
  114. package/dist/server/config.js +18 -0
  115. package/dist/server/doctor.d.ts +85 -0
  116. package/dist/server/doctor.js +280 -0
  117. package/dist/server/pool.d.ts +47 -0
  118. package/dist/server/pool.js +73 -0
  119. package/dist/skills/context.d.ts +61 -0
  120. package/dist/skills/context.js +107 -0
  121. package/dist/skills/index.d.ts +1 -0
  122. package/dist/skills/index.js +1 -0
  123. package/dist/telemetry/spans.js +5 -0
  124. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  125. package/dist/testing/conformance/artifact-exports.js +172 -0
  126. package/dist/testing/conformance/artifacts.d.ts +11 -0
  127. package/dist/testing/conformance/artifacts.js +316 -0
  128. package/dist/testing/conformance/audio.d.ts +38 -0
  129. package/dist/testing/conformance/audio.js +145 -0
  130. package/dist/testing/conformance/capability.d.ts +23 -0
  131. package/dist/testing/conformance/capability.js +27 -0
  132. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  133. package/dist/testing/conformance/checkpoint-store.js +72 -0
  134. package/dist/testing/conformance/connections.d.ts +14 -0
  135. package/dist/testing/conformance/connections.js +171 -0
  136. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  137. package/dist/testing/conformance/conversation-store.js +60 -0
  138. package/dist/testing/conformance/evaluation.d.ts +10 -0
  139. package/dist/testing/conformance/evaluation.js +295 -0
  140. package/dist/testing/conformance/files.d.ts +19 -0
  141. package/dist/testing/conformance/files.js +454 -0
  142. package/dist/testing/conformance/flows.d.ts +16 -0
  143. package/dist/testing/conformance/flows.js +193 -0
  144. package/dist/testing/conformance/graph.d.ts +22 -0
  145. package/dist/testing/conformance/graph.js +500 -0
  146. package/dist/testing/conformance/hitl.d.ts +25 -0
  147. package/dist/testing/conformance/hitl.js +523 -0
  148. package/dist/testing/conformance/index.d.ts +163 -0
  149. package/dist/testing/conformance/index.js +321 -0
  150. package/dist/testing/conformance/invariants.d.ts +23 -0
  151. package/dist/testing/conformance/invariants.js +80 -0
  152. package/dist/testing/conformance/knowledge.d.ts +41 -0
  153. package/dist/testing/conformance/knowledge.js +592 -0
  154. package/dist/testing/conformance/parents.d.ts +50 -0
  155. package/dist/testing/conformance/parents.js +39 -0
  156. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  157. package/dist/testing/conformance/rate-limit.js +71 -0
  158. package/dist/testing/conformance/records.d.ts +36 -0
  159. package/dist/testing/conformance/records.js +400 -0
  160. package/dist/testing/conformance/rollups.d.ts +30 -0
  161. package/dist/testing/conformance/rollups.js +425 -0
  162. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  163. package/dist/testing/conformance/run-coordinator.js +150 -0
  164. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  165. package/dist/testing/conformance/run-event-log.js +138 -0
  166. package/dist/testing/conformance/run-store.d.ts +12 -0
  167. package/dist/testing/conformance/run-store.js +294 -0
  168. package/dist/testing/conformance/session-state.d.ts +38 -0
  169. package/dist/testing/conformance/session-state.js +198 -0
  170. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  171. package/dist/testing/conformance/usage-limits.js +309 -0
  172. package/dist/testing/conformance.d.ts +8 -0
  173. package/dist/testing/conformance.js +8 -0
  174. package/dist/testing/memory-backend.d.ts +39 -0
  175. package/dist/testing/memory-backend.js +41 -0
  176. package/dist/testing/pglite.d.ts +55 -0
  177. package/dist/testing/pglite.js +78 -0
  178. package/dist/testing/stub-model.d.ts +58 -0
  179. package/dist/testing/stub-model.js +71 -0
  180. package/dist/testing/supabase-storage-double.d.ts +35 -0
  181. package/dist/testing/supabase-storage-double.js +101 -0
  182. package/dist/toolkit/http.d.ts +27 -0
  183. package/dist/toolkit/http.js +48 -0
  184. package/dist/toolkit/index.d.ts +27 -1
  185. package/dist/toolkit/index.js +24 -1
  186. package/dist/toolkit/mime.d.ts +97 -0
  187. package/dist/toolkit/mime.js +233 -0
  188. package/dist/toolkit/ssrf.d.ts +139 -0
  189. package/dist/toolkit/ssrf.js +349 -0
  190. package/dist/toolkit/vendor.d.ts +91 -0
  191. package/dist/toolkit/vendor.js +153 -0
  192. package/dist/tools/credentials.d.ts +265 -6
  193. package/dist/tools/credentials.js +318 -3
  194. package/dist/tools/define.d.ts +14 -0
  195. package/dist/tools/define.js +3 -0
  196. package/dist/tools/index.d.ts +21 -0
  197. package/dist/tools/index.js +7 -0
  198. package/dist/tools/library/audio.d.ts +63 -0
  199. package/dist/tools/library/audio.js +148 -0
  200. package/dist/tools/library/index.d.ts +8 -0
  201. package/dist/tools/library/index.js +7 -0
  202. package/dist/tools/registry.d.ts +33 -0
  203. package/dist/tools/registry.js +72 -2
  204. package/dist/usage/index.d.ts +21 -0
  205. package/dist/usage/index.js +1 -0
  206. package/dist/usage/rate-limit.d.ts +122 -0
  207. package/dist/usage/rate-limit.js +131 -0
  208. package/dist/usage/recorder.js +3 -0
  209. package/package.json +29 -2
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `GraphStore` conformance — REQ-064 (#270), task #271.
3
+ *
4
+ * Every clause here is a defect somebody would otherwise ship, and three are the reason the port has the shape
5
+ * it does:
6
+ *
7
+ * - **A source's contribution is what gets replaced.** Entities are shared between sources, so re-indexing one
8
+ * document must withdraw its claims and leave everything else standing. An adapter that deleted entities by
9
+ * source would erase a concept the rest of the corpus still talks about.
10
+ * - **Provenance cannot be empty.** The retriever presents graph material as citable, so an untraceable claim
11
+ * is one the model states as though a document said it. Enforced here so it is a *contract*, not a Postgres
12
+ * constraint the reference adapter quietly tolerates.
13
+ * - **`fingerprint` is on the port.** Determinism is what #275 measures against, and a test that serialised the
14
+ * graph itself would prove the reference adapter deterministic while saying nothing about Postgres — where
15
+ * row order is precisely the thing most likely to differ.
16
+ *
17
+ * Ungated: none of this needs pgvector. Entities and edges are ordinary rows, and a deployment without the
18
+ * extension can still run `graph-local`.
19
+ */
20
+ import type { GraphStore } from "../../persistence/index.js";
21
+ export declare function graphStoreConformance(make: () => GraphStore): void;
22
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1,500 @@
1
+ /**
2
+ * `GraphStore` conformance — REQ-064 (#270), task #271.
3
+ *
4
+ * Every clause here is a defect somebody would otherwise ship, and three are the reason the port has the shape
5
+ * it does:
6
+ *
7
+ * - **A source's contribution is what gets replaced.** Entities are shared between sources, so re-indexing one
8
+ * document must withdraw its claims and leave everything else standing. An adapter that deleted entities by
9
+ * source would erase a concept the rest of the corpus still talks about.
10
+ * - **Provenance cannot be empty.** The retriever presents graph material as citable, so an untraceable claim
11
+ * is one the model states as though a document said it. Enforced here so it is a *contract*, not a Postgres
12
+ * constraint the reference adapter quietly tolerates.
13
+ * - **`fingerprint` is on the port.** Determinism is what #275 measures against, and a test that serialised the
14
+ * graph itself would prove the reference adapter deterministic while saying nothing about Postgres — where
15
+ * row order is precisely the thing most likely to differ.
16
+ *
17
+ * Ungated: none of this needs pgvector. Entities and edges are ordinary rows, and a deployment without the
18
+ * extension can still run `graph-local`.
19
+ */
20
+ import { describe, expect, it } from "vitest";
21
+ import { asId } from "../../core/ids.js";
22
+ const T1 = asId("conf-graph-tenant-1");
23
+ const T2 = asId("conf-graph-tenant-2");
24
+ const AT = "2026-08-28T12:00:00.000Z";
25
+ const entity = (id, over = {}) => ({
26
+ id,
27
+ name: id.slice(id.indexOf(":") + 1),
28
+ type: id.slice(0, id.indexOf(":")),
29
+ surfaceForms: [id.slice(id.indexOf(":") + 1)],
30
+ provenance: ["chunk-1"],
31
+ ...over,
32
+ });
33
+ const contribution = (entities, edges = []) => ({
34
+ entities,
35
+ relationships: edges.map((edge) => ({
36
+ id: `${edge.from}|${edge.type ?? "rel"}|${edge.to}`,
37
+ fromId: edge.from,
38
+ toId: edge.to,
39
+ type: edge.type ?? "rel",
40
+ weight: (edge.provenance ?? ["chunk-1"]).length,
41
+ provenance: edge.provenance ?? ["chunk-1"],
42
+ })),
43
+ });
44
+ // `export function`, not an arrow const: the isolation guard in `conformance-coverage.test.ts` splits
45
+ // harness sources on that exact form to find each harness body. An arrow const is invisible to it, and
46
+ // the harness would then be registered but never checked for a cross-tenant assertion.
47
+ export function graphStoreConformance(make) {
48
+ describe("GraphStore conformance", () => {
49
+ describe("the two-level switch", () => {
50
+ it("is off for a tenant nobody configured", async () => {
51
+ // Absent must mean off. A tenant that has never been configured must not start paying for extraction
52
+ // because a row was missing rather than false.
53
+ expect((await make().getSettings({ tenantId: T1 })).enabled).toBe(false);
54
+ });
55
+ it("remembers being switched on, and off again", async () => {
56
+ const store = make();
57
+ expect((await store.setEnabled({ tenantId: T1, enabled: true, at: AT })).enabled).toBe(true);
58
+ expect((await store.getSettings({ tenantId: T1 })).enabled).toBe(true);
59
+ await store.setEnabled({ tenantId: T1, enabled: false, at: AT });
60
+ expect((await store.getSettings({ tenantId: T1 })).enabled).toBe(false);
61
+ });
62
+ it("keeps one tenant's switch away from another's", async () => {
63
+ const store = make();
64
+ await store.setEnabled({ tenantId: T1, enabled: true, at: AT });
65
+ expect((await store.getSettings({ tenantId: T2 })).enabled).toBe(false);
66
+ });
67
+ it("stores a source flag independently of the tenant switch", async () => {
68
+ /**
69
+ * The scenario the two levels exist for: mark the handbook today, enable the tenant next week, re-mark
70
+ * nothing. An adapter that refused or dropped the flag while the tenant switch was off would make that
71
+ * impossible and the failure would only appear weeks later.
72
+ */
73
+ const store = make();
74
+ await store.setSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "handbook", enabled: true });
75
+ expect((await store.getSettings({ tenantId: T1 })).enabled).toBe(false);
76
+ expect(await store.isSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "handbook" })).toBe(true);
77
+ });
78
+ it("reports an unflagged source as off rather than absent", async () => {
79
+ expect(await make().isSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "never-mentioned" })).toBe(false);
80
+ });
81
+ it("lists only the flagged sources, and only this tenant's", async () => {
82
+ const store = make();
83
+ await store.setSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "a", enabled: true });
84
+ await store.setSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "b", enabled: false });
85
+ await store.setSourceEnabled({ tenantId: T2, sourceType: "file", sourceId: "c", enabled: true });
86
+ expect((await store.listEnabledSources({ tenantId: T1, limit: 10 })).items).toEqual([
87
+ { sourceType: "file", sourceId: "a" },
88
+ ]);
89
+ });
90
+ it("un-flags a source without forgetting it", async () => {
91
+ const store = make();
92
+ await store.setSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "a", enabled: true });
93
+ await store.setSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "a", enabled: false });
94
+ expect(await store.isSourceEnabled({ tenantId: T1, sourceType: "file", sourceId: "a" })).toBe(false);
95
+ expect((await store.listEnabledSources({ tenantId: T1, limit: 10 })).items).toEqual([]);
96
+ });
97
+ });
98
+ describe("contributions and pruning", () => {
99
+ it("merges what two sources say about the same entity", async () => {
100
+ const store = make();
101
+ await store.replaceSourceGraph({
102
+ tenantId: T1,
103
+ sourceType: "file",
104
+ sourceId: "a",
105
+ contribution: contribution([entity("concept:shared", { surfaceForms: ["Shared"], provenance: ["a1"] })]),
106
+ });
107
+ await store.replaceSourceGraph({
108
+ tenantId: T1,
109
+ sourceType: "file",
110
+ sourceId: "b",
111
+ contribution: contribution([entity("concept:shared", { surfaceForms: ["shared"], provenance: ["b1"] })]),
112
+ });
113
+ const merged = await store.getEntity({ tenantId: T1, id: "concept:shared" });
114
+ expect(merged?.provenance).toEqual(["a1", "b1"]);
115
+ expect(merged?.surfaceForms).toEqual(["Shared", "shared"]);
116
+ });
117
+ it("prunes only what no other source still names", async () => {
118
+ const store = make();
119
+ await store.replaceSourceGraph({
120
+ tenantId: T1,
121
+ sourceType: "file",
122
+ sourceId: "a",
123
+ contribution: contribution([entity("concept:shared"), entity("concept:only-a")]),
124
+ });
125
+ await store.replaceSourceGraph({
126
+ tenantId: T1,
127
+ sourceType: "file",
128
+ sourceId: "b",
129
+ contribution: contribution([entity("concept:shared")]),
130
+ });
131
+ await store.deleteSourceGraph({ tenantId: T1, sourceType: "file", sourceId: "a" });
132
+ const left = (await store.listEntities({ tenantId: T1, limit: 50 })).items.map((e) => e.id);
133
+ // `shared` survives because B still names it; `only-a` goes because nothing does.
134
+ expect(left).toEqual(["concept:shared"]);
135
+ });
136
+ it("reports the counts of the whole graph, not just the source that was written", async () => {
137
+ /**
138
+ * The returned counts are what an operator reads to see whether a re-index lost anything, so they have
139
+ * to describe the graph rather than the call. An implementation that counted only the incoming
140
+ * contribution would report "1 entity" for a corpus of thousands — and every assertion about a
141
+ * single-source graph would still pass, which is why this needs two sources.
142
+ */
143
+ const store = make();
144
+ await store.replaceSourceGraph({
145
+ tenantId: T1,
146
+ sourceType: "file",
147
+ sourceId: "a",
148
+ contribution: contribution([entity("concept:a"), entity("concept:b")], [{ from: "concept:a", to: "concept:b" }]),
149
+ });
150
+ const second = await store.replaceSourceGraph({
151
+ tenantId: T1,
152
+ sourceType: "file",
153
+ sourceId: "b",
154
+ contribution: contribution([entity("concept:c")]),
155
+ });
156
+ expect(second.entities).toBe(3);
157
+ expect(second.relationships).toBe(1);
158
+ });
159
+ it("replaces a source's contribution rather than appending to it", async () => {
160
+ const store = make();
161
+ await store.replaceSourceGraph({
162
+ tenantId: T1,
163
+ sourceType: "file",
164
+ sourceId: "a",
165
+ contribution: contribution([entity("concept:old")]),
166
+ });
167
+ await store.replaceSourceGraph({
168
+ tenantId: T1,
169
+ sourceType: "file",
170
+ sourceId: "a",
171
+ contribution: contribution([entity("concept:new")]),
172
+ });
173
+ // A re-indexed document must not leave its old claims standing.
174
+ expect((await store.listEntities({ tenantId: T1, limit: 50 })).items.map((e) => e.id)).toEqual(["concept:new"]);
175
+ });
176
+ it("treats an empty contribution as a withdrawal", async () => {
177
+ const store = make();
178
+ await store.replaceSourceGraph({
179
+ tenantId: T1,
180
+ sourceType: "file",
181
+ sourceId: "a",
182
+ contribution: contribution([entity("concept:x")]),
183
+ });
184
+ await store.replaceSourceGraph({
185
+ tenantId: T1,
186
+ sourceType: "file",
187
+ sourceId: "a",
188
+ contribution: { entities: [], relationships: [] },
189
+ });
190
+ expect(await store.fingerprint({ tenantId: T1 })).toBe("");
191
+ });
192
+ it("drops an edge when an endpoint stops existing", async () => {
193
+ const store = make();
194
+ await store.replaceSourceGraph({
195
+ tenantId: T1,
196
+ sourceType: "file",
197
+ sourceId: "a",
198
+ contribution: contribution([entity("concept:a"), entity("concept:b")], [{ from: "concept:a", to: "concept:b" }]),
199
+ });
200
+ expect(await store.neighbours({ tenantId: T1, entityIds: ["concept:a"], limit: 10 })).toHaveLength(1);
201
+ await store.replaceSourceGraph({
202
+ tenantId: T1,
203
+ sourceType: "file",
204
+ sourceId: "a",
205
+ contribution: contribution([entity("concept:a")]),
206
+ });
207
+ // An edge pointing at nothing is worse than no edge: a traversal would follow it and find a hole.
208
+ expect(await store.neighbours({ tenantId: T1, entityIds: ["concept:a"], limit: 10 })).toEqual([]);
209
+ });
210
+ it("refuses an entity with no provenance", async () => {
211
+ await expect(make().replaceSourceGraph({
212
+ tenantId: T1,
213
+ sourceType: "file",
214
+ sourceId: "a",
215
+ contribution: contribution([entity("concept:x", { provenance: [] })]),
216
+ })).rejects.toThrow();
217
+ });
218
+ it("refuses a relationship with no provenance", async () => {
219
+ await expect(make().replaceSourceGraph({
220
+ tenantId: T1,
221
+ sourceType: "file",
222
+ sourceId: "a",
223
+ contribution: contribution([entity("concept:a"), entity("concept:b")], [{ from: "concept:a", to: "concept:b", provenance: [] }]),
224
+ })).rejects.toThrow();
225
+ });
226
+ });
227
+ describe("reading", () => {
228
+ const seeded = async () => {
229
+ const store = make();
230
+ await store.replaceSourceGraph({
231
+ tenantId: T1,
232
+ sourceType: "file",
233
+ sourceId: "a",
234
+ contribution: contribution([entity("concept:hub"), entity("concept:light"), entity("person:heavy", { provenance: ["c1", "c2"] })], [
235
+ { from: "concept:hub", to: "concept:light", provenance: ["c1"] },
236
+ { from: "concept:hub", to: "person:heavy", provenance: ["c1", "c2"] },
237
+ ]),
238
+ });
239
+ return store;
240
+ };
241
+ it("returns null for an entity that does not exist", async () => {
242
+ expect(await (await seeded()).getEntity({ tenantId: T1, id: "concept:nope" })).toBeNull();
243
+ });
244
+ it("fetches entities by id and ignores the ones that are not there", async () => {
245
+ const found = await (await seeded()).getEntities({ tenantId: T1, ids: ["concept:hub", "concept:nope"] });
246
+ expect(found.map((e) => e.id)).toEqual(["concept:hub"]);
247
+ });
248
+ it("returns nothing for an empty id list rather than everything", async () => {
249
+ // The `authSubjects: []` lesson from `VectorIndex`: an empty filter must mean "none", never "all".
250
+ expect(await (await seeded()).getEntities({ tenantId: T1, ids: [] })).toEqual([]);
251
+ expect(await (await seeded()).neighbours({ tenantId: T1, entityIds: [], limit: 10 })).toEqual([]);
252
+ });
253
+ it("resolves an entity by normalised name across types — #273", async () => {
254
+ /**
255
+ * The query-side lookup. A question says "the retry budget" and cannot say which *type* the graph
256
+ * assigned, so resolution is by name alone. The adapter matches on everything after the first colon in
257
+ * the id, which is where the index-side normaliser already put the normalised name — so the two sides
258
+ * agree by construction rather than by two functions being kept in step.
259
+ */
260
+ const store = await seeded();
261
+ const found = await store.resolveEntities({ tenantId: T1, normalisedNames: ["hub", "heavy"] });
262
+ // Both types come back, which is the point: the caller did not have to know `concept` from `person`.
263
+ expect(found.map((e) => e.id).sort()).toEqual(["concept:hub", "person:heavy"]);
264
+ });
265
+ it("returns nothing for an empty name list rather than everything", async () => {
266
+ expect(await (await seeded()).resolveEntities({ tenantId: T1, normalisedNames: [] })).toEqual([]);
267
+ });
268
+ it("does not resolve a name from another tenant", async () => {
269
+ const store = await seeded();
270
+ expect(await store.resolveEntities({ tenantId: T2, normalisedNames: ["hub"] })).toEqual([]);
271
+ });
272
+ it("filters entities by type", async () => {
273
+ const found = await (await seeded()).listEntities({ tenantId: T1, limit: 50, type: "person" });
274
+ expect(found.items.map((e) => e.id)).toEqual(["person:heavy"]);
275
+ });
276
+ it("lists entities in a stable order and pages through them", async () => {
277
+ const store = await seeded();
278
+ const first = await store.listEntities({ tenantId: T1, limit: 2 });
279
+ expect(first.items.map((e) => e.id)).toEqual(["concept:hub", "concept:light"]);
280
+ expect(first.nextCursor).toBeDefined();
281
+ const second = await store.listEntities({ tenantId: T1, limit: 2, cursor: first.nextCursor });
282
+ expect(second.items.map((e) => e.id)).toEqual(["person:heavy"]);
283
+ expect(second.nextCursor).toBeUndefined();
284
+ });
285
+ it("finds edges in either direction", async () => {
286
+ // Traversal reads by both endpoints. An adapter indexing only `from_id` would make half of every
287
+ // neighbourhood query return nothing.
288
+ const store = await seeded();
289
+ expect(await store.neighbours({ tenantId: T1, entityIds: ["concept:light"], limit: 10 })).toHaveLength(1);
290
+ });
291
+ it("orders neighbours heaviest first, so a truncated traversal keeps the best edges", async () => {
292
+ const store = await seeded();
293
+ const [first] = await store.neighbours({ tenantId: T1, entityIds: ["concept:hub"], limit: 1 });
294
+ expect(first?.toId).toBe("person:heavy");
295
+ });
296
+ it("honours the neighbour limit", async () => {
297
+ expect(await (await seeded()).neighbours({ tenantId: T1, entityIds: ["concept:hub"], limit: 1 })).toHaveLength(1);
298
+ expect(await (await seeded()).neighbours({ tenantId: T1, entityIds: ["concept:hub"], limit: 0 })).toEqual([]);
299
+ });
300
+ });
301
+ describe("communities — #272", () => {
302
+ const community = (id, over = {}) => ({
303
+ id,
304
+ level: 0,
305
+ entityIds: ["concept:a"],
306
+ relationshipIds: [],
307
+ chunkIds: ["c1"],
308
+ fingerprint: `fp-${id}`,
309
+ ...over,
310
+ });
311
+ it("stores and lists a hierarchy, ordered by level then id", async () => {
312
+ const store = make();
313
+ await store.replaceCommunities({
314
+ tenantId: T1,
315
+ communities: [community("L1:a", { level: 1 }), community("L0:b"), community("L0:a")],
316
+ });
317
+ const listed = (await store.listCommunities({ tenantId: T1, limit: 10 })).items.map((c) => c.id);
318
+ expect(listed).toEqual(["L0:a", "L0:b", "L1:a"]);
319
+ });
320
+ it("filters by level, which is how graph-global picks its granularity", async () => {
321
+ const store = make();
322
+ await store.replaceCommunities({
323
+ tenantId: T1,
324
+ communities: [community("L0:a"), community("L1:a", { level: 1 })],
325
+ });
326
+ expect((await store.listCommunities({ tenantId: T1, limit: 10, level: 1 })).items.map((c) => c.id)).toEqual([
327
+ "L1:a",
328
+ ]);
329
+ });
330
+ it("records a summary against the fingerprint it was written for", async () => {
331
+ const store = make();
332
+ await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a")] });
333
+ await store.setCommunitySummary({ tenantId: T1, id: "L0:a", summary: "about a", fingerprint: "fp-L0:a", at: AT });
334
+ const stored = await store.getCommunity({ tenantId: T1, id: "L0:a" });
335
+ expect(stored?.summary).toBe("about a");
336
+ expect(stored?.summaryFingerprint).toBe("fp-L0:a");
337
+ expect(stored?.summarisedAt).toBe(AT);
338
+ });
339
+ it("keeps a summary across a rebuild when the membership is identical", async () => {
340
+ // The incremental saving: one model call per community is the cost worth avoiding.
341
+ const store = make();
342
+ await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a")] });
343
+ await store.setCommunitySummary({ tenantId: T1, id: "L0:a", summary: "kept", fingerprint: "fp-L0:a", at: AT });
344
+ const again = await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a")] });
345
+ expect(again.summariesKept).toBe(1);
346
+ expect((await store.getCommunity({ tenantId: T1, id: "L0:a" }))?.summary).toBe("kept");
347
+ });
348
+ it("discards a summary when the membership changed, even though the id did not", async () => {
349
+ /**
350
+ * The dangerous case. A community's id comes from its smallest member, so it can keep its id while
351
+ * gaining members — and a summary of the old membership attached to the new one is a confidently wrong
352
+ * description, which is worse than none at all.
353
+ */
354
+ const store = make();
355
+ await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a")] });
356
+ await store.setCommunitySummary({ tenantId: T1, id: "L0:a", summary: "stale", fingerprint: "fp-L0:a", at: AT });
357
+ const again = await store.replaceCommunities({
358
+ tenantId: T1,
359
+ communities: [community("L0:a", { entityIds: ["concept:a", "concept:z"], fingerprint: "fp-changed" })],
360
+ });
361
+ expect(again.summariesKept).toBe(0);
362
+ expect((await store.getCommunity({ tenantId: T1, id: "L0:a" }))?.summary).toBeUndefined();
363
+ });
364
+ it("replaces the whole hierarchy rather than merging into it", async () => {
365
+ // Clustering is global: a partial write leaves levels that disagree about who is in what.
366
+ const store = make();
367
+ await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a"), community("L0:b")] });
368
+ await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a")] });
369
+ expect((await store.listCommunities({ tenantId: T1, limit: 10 })).items.map((c) => c.id)).toEqual(["L0:a"]);
370
+ });
371
+ it("returns null for a community that does not exist", async () => {
372
+ expect(await make().getCommunity({ tenantId: T1, id: "nope" })).toBeNull();
373
+ });
374
+ it("ignores a summary written for a community that is gone", async () => {
375
+ // A rebuild can drop a community between the clustering and the summarisation of it. Writing the
376
+ // summary anyway would resurrect a row nothing else references.
377
+ const store = make();
378
+ await store.setCommunitySummary({ tenantId: T1, id: "vanished", summary: "s", fingerprint: "f", at: AT });
379
+ expect(await store.getCommunity({ tenantId: T1, id: "vanished" })).toBeNull();
380
+ });
381
+ it("keeps one tenant's communities away from another's", async () => {
382
+ const store = make();
383
+ await store.replaceCommunities({ tenantId: T1, communities: [community("L0:a")] });
384
+ expect((await store.listCommunities({ tenantId: T2, limit: 10 })).items).toEqual([]);
385
+ expect(await store.getCommunity({ tenantId: T2, id: "L0:a" })).toBeNull();
386
+ });
387
+ });
388
+ describe("tenant isolation", () => {
389
+ it("keeps every read away from another tenant's graph", async () => {
390
+ const store = make();
391
+ await store.replaceSourceGraph({
392
+ tenantId: T1,
393
+ sourceType: "file",
394
+ sourceId: "a",
395
+ contribution: contribution([entity("concept:secret"), entity("concept:other")], [
396
+ { from: "concept:secret", to: "concept:other" },
397
+ ]),
398
+ });
399
+ // Every read path, not a sample: a graph leaks through whichever one somebody forgot to scope.
400
+ expect((await store.listEntities({ tenantId: T2, limit: 50 })).items).toEqual([]);
401
+ expect(await store.getEntity({ tenantId: T2, id: "concept:secret" })).toBeNull();
402
+ expect(await store.getEntities({ tenantId: T2, ids: ["concept:secret"] })).toEqual([]);
403
+ expect(await store.neighbours({ tenantId: T2, entityIds: ["concept:secret"], limit: 10 })).toEqual([]);
404
+ expect(await store.fingerprint({ tenantId: T2 })).toBe("");
405
+ });
406
+ it("does not let one tenant's replacement prune another's entities", async () => {
407
+ const store = make();
408
+ for (const tenantId of [T1, T2]) {
409
+ await store.replaceSourceGraph({
410
+ tenantId,
411
+ sourceType: "file",
412
+ sourceId: "a",
413
+ contribution: contribution([entity("concept:shared")]),
414
+ });
415
+ }
416
+ await store.replaceSourceGraph({
417
+ tenantId: T1,
418
+ sourceType: "file",
419
+ sourceId: "a",
420
+ contribution: { entities: [], relationships: [] },
421
+ });
422
+ expect((await store.listEntities({ tenantId: T2, limit: 50 })).items.map((e) => e.id)).toEqual(["concept:shared"]);
423
+ });
424
+ });
425
+ describe("fingerprint", () => {
426
+ it("is empty for an empty graph", async () => {
427
+ expect(await make().fingerprint({ tenantId: T1 })).toBe("");
428
+ });
429
+ it("is identical for the same graph written twice", async () => {
430
+ const build = async () => {
431
+ const store = make();
432
+ await store.replaceSourceGraph({
433
+ tenantId: T1,
434
+ sourceType: "file",
435
+ sourceId: "a",
436
+ contribution: contribution([entity("concept:a", { provenance: ["c2", "c1"] }), entity("concept:b")], [{ from: "concept:a", to: "concept:b", provenance: ["c2", "c1"] }]),
437
+ });
438
+ return store.fingerprint({ tenantId: T1 });
439
+ };
440
+ const first = await build();
441
+ expect(await build()).toBe(first);
442
+ expect(first).not.toBe("");
443
+ });
444
+ it("does not depend on the order sources were written in", async () => {
445
+ // Re-indexing visits sources in whatever order the work list produced. A fingerprint that changed with
446
+ // it would make #275's baseline unusable and every rebuild look like a diff.
447
+ const build = async (order) => {
448
+ const store = make();
449
+ for (const sourceId of order) {
450
+ await store.replaceSourceGraph({
451
+ tenantId: T1,
452
+ sourceType: "file",
453
+ sourceId,
454
+ contribution: contribution([entity(`concept:${sourceId}`), entity("concept:shared")]),
455
+ });
456
+ }
457
+ return store.fingerprint({ tenantId: T1 });
458
+ };
459
+ expect(await build(["a", "b"])).toBe(await build(["b", "a"]));
460
+ });
461
+ it("changes when the graph does", async () => {
462
+ // A fingerprint that never changed would satisfy every assertion above and detect nothing.
463
+ const store = make();
464
+ await store.replaceSourceGraph({
465
+ tenantId: T1,
466
+ sourceType: "file",
467
+ sourceId: "a",
468
+ contribution: contribution([entity("concept:a")]),
469
+ });
470
+ const before = await store.fingerprint({ tenantId: T1 });
471
+ await store.replaceSourceGraph({
472
+ tenantId: T1,
473
+ sourceType: "file",
474
+ sourceId: "b",
475
+ contribution: contribution([entity("concept:b")]),
476
+ });
477
+ expect(await store.fingerprint({ tenantId: T1 })).not.toBe(before);
478
+ });
479
+ it("notices a difference in provenance alone", async () => {
480
+ /**
481
+ * The clause that caught a real bug elsewhere: provenance and surface forms are *sets*, arriving in
482
+ * whatever order extraction produced. Leaving them out of the fingerprint would let it agree while the
483
+ * graph differed — which is worse than no fingerprint, because it would be trusted.
484
+ */
485
+ const build = async (provenance) => {
486
+ const store = make();
487
+ await store.replaceSourceGraph({
488
+ tenantId: T1,
489
+ sourceType: "file",
490
+ sourceId: "a",
491
+ contribution: contribution([entity("concept:a", { provenance })]),
492
+ });
493
+ return store.fingerprint({ tenantId: T1 });
494
+ };
495
+ expect(await build(["c1"])).not.toBe(await build(["c1", "c2"]));
496
+ });
497
+ });
498
+ });
499
+ }
500
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `InteractionStore`, `ApprovalGrantStore` and `UsageStore` conformance.
3
+ *
4
+ * The two properties here are the ones that would cost real money or cause a real duplicate
5
+ * publish if an adapter got them wrong, so they are asserted rather than trusted:
6
+ *
7
+ * - **Idempotent resolution** (`docs/04`): "the first call resolves the interaction and reports
8
+ * `alreadyResolved: false`; a duplicate reports `true` and changes nothing, so a continuation is
9
+ * queued exactly once." A store that reports `false` twice queues the continuation twice.
10
+ * - **Grant scoping** (`docs/04`): a `conversation`-scoped grant "never leaks to another
11
+ * conversation or tenant-wide". A store that ignores `conversationId` silently converts a
12
+ * one-conversation approval into a standing one.
13
+ * - **Append-only usage** (`docs/12`): events are never edited; appends are idempotent on
14
+ * `(runId, stepId)` so a recovered run never double-counts.
15
+ * - **The approval claim** (`docs/04` → How the loop closes): an approval's single execution is
16
+ * claimed exactly once, and only after a decision. This is where `allow-once` gets its "once" —
17
+ * it issues no grant, so an adapter that lost the claim would let one decision publish twice, and
18
+ * one that let an undecided interaction be claimed would create permission out of nothing.
19
+ */
20
+ import type { ApprovalGrantStore, InteractionStore, UsageStore } from "../../persistence/index.js";
21
+ import { type FixtureOrStore } from "./parents.js";
22
+ export declare function interactionStoreConformance(makeFixture: () => FixtureOrStore<InteractionStore>): void;
23
+ export declare function approvalGrantStoreConformance(makeFixture: () => FixtureOrStore<ApprovalGrantStore>): void;
24
+ export declare function usageStoreConformance(makeFixture: () => FixtureOrStore<UsageStore>): void;
25
+ //# sourceMappingURL=hitl.d.ts.map