devcontour-harness 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (263) hide show
  1. package/README.md +42 -2
  2. package/docs/compatibility-versions.md +56 -0
  3. package/docs/compatibility.md +2 -2
  4. package/docs/connectors.md +323 -0
  5. package/docs/contracts.md +33 -5
  6. package/docs/integrations/hindsight.md +148 -0
  7. package/docs/quickstart.md +1 -1
  8. package/docs/release-notes/0.1.0-alpha.6.md +238 -0
  9. package/docs/release-notes/0.1.0-alpha.7.md +71 -0
  10. package/docs/review-probes.md +199 -0
  11. package/docs/review-protocol.md +251 -0
  12. package/docs/toolchains.md +167 -0
  13. package/docs/turbo-starter.md +269 -0
  14. package/docs/upgrade.md +77 -24
  15. package/examples/README.md +1 -0
  16. package/examples/toolchains/README.md +32 -0
  17. package/examples/toolchains/go/bad/label/label.go +9 -0
  18. package/examples/toolchains/go/good/label/label.go +9 -0
  19. package/examples/toolchains/go/policy.json +79 -0
  20. package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
  21. package/examples/toolchains/go/repo/_gitignore +1 -0
  22. package/examples/toolchains/go/repo/go.mod +5 -0
  23. package/examples/toolchains/go/repo/go.sum +2 -0
  24. package/examples/toolchains/go/repo/label/label.go +9 -0
  25. package/examples/toolchains/go/repo/label/label_test.go +27 -0
  26. package/examples/toolchains/go/repo/label/version.go +9 -0
  27. package/examples/toolchains/go/task.json +25 -0
  28. package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
  29. package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
  30. package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
  31. package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
  32. package/examples/toolchains/mixed-go/policy.json +126 -0
  33. package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
  34. package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
  35. package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
  36. package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
  37. package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
  38. package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
  39. package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
  40. package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
  41. package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
  42. package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
  43. package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
  44. package/examples/toolchains/mixed-go/task.json +25 -0
  45. package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
  46. package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
  47. package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
  48. package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
  49. package/examples/toolchains/mixed-python/policy.json +121 -0
  50. package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
  51. package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
  52. package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
  53. package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
  54. package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
  55. package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
  56. package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
  57. package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
  58. package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
  59. package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
  60. package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
  61. package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
  62. package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
  63. package/examples/toolchains/mixed-python/task.json +25 -0
  64. package/examples/toolchains/npm/bad/src/label.js +4 -0
  65. package/examples/toolchains/npm/good/src/label.js +4 -0
  66. package/examples/toolchains/npm/policy.json +82 -0
  67. package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
  68. package/examples/toolchains/npm/repo/_gitignore +4 -0
  69. package/examples/toolchains/npm/repo/package-lock.json +22 -0
  70. package/examples/toolchains/npm/repo/package.json +13 -0
  71. package/examples/toolchains/npm/repo/src/label.js +4 -0
  72. package/examples/toolchains/npm/repo/test/label.test.js +13 -0
  73. package/examples/toolchains/npm/task.json +25 -0
  74. package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
  75. package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
  76. package/examples/toolchains/pnpm/policy.json +85 -0
  77. package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
  78. package/examples/toolchains/pnpm/repo/_gitignore +4 -0
  79. package/examples/toolchains/pnpm/repo/package.json +10 -0
  80. package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
  81. package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
  82. package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
  83. package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
  84. package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
  85. package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
  86. package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
  87. package/examples/toolchains/pnpm/task.json +25 -0
  88. package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
  89. package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
  90. package/examples/toolchains/python/policy.json +80 -0
  91. package/examples/toolchains/python/repo/.python-version +1 -0
  92. package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
  93. package/examples/toolchains/python/repo/_gitignore +5 -0
  94. package/examples/toolchains/python/repo/pyproject.toml +19 -0
  95. package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
  96. package/examples/toolchains/python/repo/tests/test_label.py +13 -0
  97. package/examples/toolchains/python/repo/uv.lock +108 -0
  98. package/examples/toolchains/python/task.json +25 -0
  99. package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
  100. package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
  101. package/examples/toolchains/turbo/policy.json +110 -0
  102. package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
  103. package/examples/toolchains/turbo/repo/_gitignore +4 -0
  104. package/examples/toolchains/turbo/repo/package.json +13 -0
  105. package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
  106. package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
  107. package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
  108. package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
  109. package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
  110. package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
  111. package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
  112. package/examples/toolchains/turbo/repo/turbo.json +7 -0
  113. package/examples/toolchains/turbo/task.json +25 -0
  114. package/examples/turbo-starter/README.md +16 -0
  115. package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
  116. package/examples/turbo-starter/changes/issued-at.patch +130 -0
  117. package/examples/turbo-starter/cleanup.mjs +58 -0
  118. package/examples/turbo-starter/graph.json +32 -0
  119. package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
  120. package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
  121. package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
  122. package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
  123. package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
  124. package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
  125. package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
  126. package/examples/turbo-starter/harness/env/backend.env.template +7 -0
  127. package/examples/turbo-starter/harness/env/compose.env.template +6 -0
  128. package/examples/turbo-starter/policy.json +109 -0
  129. package/examples/turbo-starter/policy.lifecycle.json +114 -0
  130. package/examples/turbo-starter/setup.mjs +352 -0
  131. package/lib/agents/cli-adapter.js +10 -1
  132. package/lib/agents/inspect.d.ts +56 -2
  133. package/lib/agents/inspect.js +264 -14
  134. package/lib/agents/review-probe.d.ts +229 -7
  135. package/lib/agents/review-probe.js +830 -72
  136. package/lib/agents/schemas.d.ts +3 -1
  137. package/lib/agents/schemas.js +7 -3
  138. package/lib/cli.js +167 -7
  139. package/lib/contracts/cli.d.ts +820 -11
  140. package/lib/contracts/cli.js +243 -4
  141. package/lib/contracts/errors.d.ts +3 -1
  142. package/lib/contracts/errors.js +2 -0
  143. package/lib/contracts/events.d.ts +37 -5
  144. package/lib/contracts/events.js +9 -0
  145. package/lib/contracts/execution.d.ts +389 -15
  146. package/lib/contracts/execution.js +76 -4
  147. package/lib/contracts/failure.js +5 -0
  148. package/lib/contracts/index.d.ts +1 -0
  149. package/lib/contracts/index.js +1 -0
  150. package/lib/contracts/policy.d.ts +15 -0
  151. package/lib/contracts/policy.js +10 -0
  152. package/lib/contracts/ports.d.ts +50 -0
  153. package/lib/contracts/profiles.d.ts +14 -0
  154. package/lib/contracts/profiles.js +4 -0
  155. package/lib/contracts/published.d.ts +279 -8
  156. package/lib/contracts/result.d.ts +171 -0
  157. package/lib/contracts/result.js +39 -1
  158. package/lib/contracts/review.d.ts +231 -0
  159. package/lib/contracts/review.js +326 -2
  160. package/lib/contracts/toolchain.d.ts +96 -0
  161. package/lib/contracts/toolchain.js +89 -0
  162. package/lib/control/doctor.d.ts +16 -1
  163. package/lib/control/doctor.js +45 -3
  164. package/lib/control/harness.d.ts +52 -6
  165. package/lib/control/harness.js +28 -1
  166. package/lib/control/recovery.d.ts +3 -1
  167. package/lib/control/review.js +23 -11
  168. package/lib/engine/budget.d.ts +3 -3
  169. package/lib/engine/classify.js +1 -0
  170. package/lib/engine/engine.d.ts +67 -17
  171. package/lib/engine/prompt.d.ts +29 -1
  172. package/lib/engine/prompt.js +57 -0
  173. package/lib/engine/property-recovery.js +2 -0
  174. package/lib/engine/result.js +52 -0
  175. package/lib/engine/reuse.js +6 -1
  176. package/lib/engine/runner.d.ts +25 -0
  177. package/lib/engine/runner.js +500 -123
  178. package/lib/environment/lifecycle.js +6 -0
  179. package/lib/environment/live-calls.js +3 -2
  180. package/lib/environment/os-tools.d.ts +16 -0
  181. package/lib/environment/os-tools.js +35 -0
  182. package/lib/environment/process.d.ts +8 -0
  183. package/lib/environment/process.js +13 -6
  184. package/lib/integrations/connector/commands.d.ts +358 -0
  185. package/lib/integrations/connector/commands.js +428 -0
  186. package/lib/integrations/connector/config.d.ts +215 -0
  187. package/lib/integrations/connector/config.js +214 -0
  188. package/lib/integrations/connector/delivery.d.ts +107 -0
  189. package/lib/integrations/connector/delivery.js +591 -0
  190. package/lib/integrations/connector/fake.d.ts +291 -0
  191. package/lib/integrations/connector/fake.js +786 -0
  192. package/lib/integrations/connector/history.d.ts +124 -0
  193. package/lib/integrations/connector/history.js +497 -0
  194. package/lib/integrations/connector/index.d.ts +16 -0
  195. package/lib/integrations/connector/index.js +16 -0
  196. package/lib/integrations/connector/redact.d.ts +19 -0
  197. package/lib/integrations/connector/redact.js +91 -0
  198. package/lib/integrations/connector/registry.d.ts +15 -0
  199. package/lib/integrations/connector/registry.js +43 -0
  200. package/lib/integrations/connector/snapshot.d.ts +131 -0
  201. package/lib/integrations/connector/snapshot.js +422 -0
  202. package/lib/integrations/connector/source.d.ts +37 -0
  203. package/lib/integrations/connector/source.js +73 -0
  204. package/lib/integrations/connector/steps.d.ts +71 -0
  205. package/lib/integrations/connector/steps.js +188 -0
  206. package/lib/integrations/connector/store.d.ts +140 -0
  207. package/lib/integrations/connector/store.js +277 -0
  208. package/lib/integrations/connector/summary.d.ts +38 -0
  209. package/lib/integrations/connector/summary.js +59 -0
  210. package/lib/integrations/connector/testing-worker.d.ts +1 -0
  211. package/lib/integrations/connector/testing-worker.js +81 -0
  212. package/lib/integrations/connector/testing.d.ts +205 -0
  213. package/lib/integrations/connector/testing.js +1615 -0
  214. package/lib/integrations/connector/transport.d.ts +240 -0
  215. package/lib/integrations/connector/transport.js +28 -0
  216. package/lib/integrations/memory/context.d.ts +120 -0
  217. package/lib/integrations/memory/context.js +239 -0
  218. package/lib/integrations/memory/index.d.ts +8 -0
  219. package/lib/integrations/memory/index.js +8 -0
  220. package/lib/integrations/memory/provider.d.ts +79 -0
  221. package/lib/integrations/memory/provider.js +22 -0
  222. package/lib/integrations/memory/sink.d.ts +56 -0
  223. package/lib/integrations/memory/sink.js +116 -0
  224. package/lib/integrations/memory/testing.d.ts +44 -0
  225. package/lib/integrations/memory/testing.js +61 -0
  226. package/lib/integrations/relay-v1/index.d.ts +122 -0
  227. package/lib/integrations/relay-v1/index.js +860 -0
  228. package/lib/integrations/relay-v2/index.d.ts +145 -0
  229. package/lib/integrations/relay-v2/index.js +752 -0
  230. package/lib/integrations/relay-v2/process.d.ts +32 -0
  231. package/lib/integrations/relay-v2/process.js +112 -0
  232. package/lib/integrations/relay-v2/protocol.d.ts +172 -0
  233. package/lib/integrations/relay-v2/protocol.js +142 -0
  234. package/lib/profiles/resolve.d.ts +5 -1
  235. package/lib/profiles/resolve.js +31 -0
  236. package/lib/storage/document.d.ts +116 -16
  237. package/lib/toolchains/index.d.ts +9 -0
  238. package/lib/toolchains/index.js +9 -0
  239. package/lib/toolchains/inspect.d.ts +105 -0
  240. package/lib/toolchains/inspect.js +831 -0
  241. package/lib/toolchains/manifests.d.ts +67 -0
  242. package/lib/toolchains/manifests.js +238 -0
  243. package/lib/toolchains/probe.d.ts +28 -0
  244. package/lib/toolchains/probe.js +54 -0
  245. package/lib/toolchains/recipes.d.ts +33 -0
  246. package/lib/toolchains/recipes.js +51 -0
  247. package/lib/verification/acceptance.d.ts +16 -0
  248. package/lib/verification/acceptance.js +16 -0
  249. package/lib/verification/gates.d.ts +38 -0
  250. package/lib/verification/gates.js +108 -26
  251. package/package.json +22 -1
  252. package/profiles/go-gotestsum.json +21 -0
  253. package/profiles/npm-ci.json +22 -0
  254. package/profiles/pnpm-frozen.json +24 -0
  255. package/profiles/python-uv.json +26 -0
  256. package/schemas/execution-document.schema.json +345 -4
  257. package/schemas/execution-policy.schema.json +48 -0
  258. package/schemas/result-envelope.schema.json +213 -1
  259. package/schemas/run-event.schema.json +93 -0
  260. package/schemas/run-result.schema.json +207 -0
  261. package/schemas/stack-profile.schema.json +43 -0
  262. package/templates/controller-mode/AGENTS.controller.md +58 -0
  263. package/templates/controller-mode/README.md +44 -0
@@ -0,0 +1,786 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { TransportError, } from "./transport.js";
5
+ export const emptyFakeState = (provider, projectId) => ({
6
+ provider,
7
+ projectId,
8
+ generation: 1,
9
+ seq: 0,
10
+ tasks: {},
11
+ comments: {},
12
+ documents: {},
13
+ sessions: {},
14
+ checkpoints: {},
15
+ mutations: {},
16
+ });
17
+ export function memoryBackend(state) {
18
+ return {
19
+ with: (fn, write) => {
20
+ if (!write)
21
+ return fn(structuredClone(state));
22
+ const draft = structuredClone(state);
23
+ const out = fn(draft);
24
+ Object.assign(state, draft);
25
+ return out;
26
+ },
27
+ close: () => undefined,
28
+ };
29
+ }
30
+ /** Состояние в файле SQLite: каждая операция — транзакция `BEGIN IMMEDIATE`. */
31
+ export function fileBackend(file, init) {
32
+ mkdirSync(dirname(file), { recursive: true });
33
+ const db = new DatabaseSync(file);
34
+ db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=20000;');
35
+ db.exec('CREATE TABLE IF NOT EXISTS state (id INTEGER PRIMARY KEY CHECK(id=1), data TEXT NOT NULL)');
36
+ if (init)
37
+ db.prepare('INSERT OR IGNORE INTO state VALUES(1, ?)').run(JSON.stringify(init));
38
+ return {
39
+ with: (fn, write) => {
40
+ db.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
41
+ try {
42
+ const row = db.prepare('SELECT data FROM state WHERE id=1').get();
43
+ const state = JSON.parse(row.data);
44
+ const out = fn(state);
45
+ if (write)
46
+ db.prepare('UPDATE state SET data=? WHERE id=1').run(JSON.stringify(state));
47
+ db.exec('COMMIT');
48
+ return out;
49
+ }
50
+ catch (error) {
51
+ if (db.isTransaction)
52
+ db.exec('ROLLBACK');
53
+ throw error;
54
+ }
55
+ },
56
+ close: () => db.close(),
57
+ };
58
+ }
59
+ const tick = () => new Promise((r) => setImmediate(r));
60
+ const columns = ['inbox', 'ready', 'in-progress', 'review', 'done', 'cancelled'];
61
+ /** Транспорт поверх подделки — тот же договор, что у настоящих адаптеров. */
62
+ export class FakeRelayTransport {
63
+ backend;
64
+ options;
65
+ provider;
66
+ /** Хук между страницами чтения задачи: тесты вносят здесь чужую запись. */
67
+ onPage;
68
+ projectId;
69
+ constructor(backend, options) {
70
+ this.backend = backend;
71
+ this.options = options;
72
+ this.provider = options.provider;
73
+ this.projectId = backend.with((s) => s.projectId, false);
74
+ }
75
+ fail(kind, operation, message, extra = {}) {
76
+ throw new TransportError(kind, operation, message, {
77
+ provider: this.provider,
78
+ projectId: this.projectId,
79
+ ...extra,
80
+ });
81
+ }
82
+ read(fn) {
83
+ return this.backend.with(fn, false);
84
+ }
85
+ write(operation, fn) {
86
+ return this.backend.with((s) => {
87
+ const out = fn(s);
88
+ s.generation++;
89
+ s.mutations[operation] = (s.mutations[operation] ?? 0) + 1;
90
+ return out;
91
+ }, true);
92
+ }
93
+ static resolve(s, ref) {
94
+ return s.tasks[ref] ?? Object.values(s.tasks).find((t) => t.key === ref);
95
+ }
96
+ taskOf(s, ref, operation) {
97
+ const task = FakeRelayTransport.resolve(s, ref);
98
+ if (!task)
99
+ this.fail('not-found', operation, 'Задача не найдена: ' + ref, { code: 'NOT_FOUND' });
100
+ return task;
101
+ }
102
+ /** Транзитивно завершена: done, все критерии, дети и зависимости. */
103
+ static finished(s, id, seen = new Set()) {
104
+ if (seen.has(id))
105
+ return false;
106
+ seen.add(id);
107
+ const t = s.tasks[id];
108
+ if (!t || t.column !== 'done' || t.criteria.some((c) => !c.completed))
109
+ return false;
110
+ const children = Object.values(s.tasks).filter((x) => x.parentId === id);
111
+ return (children.every((c) => FakeRelayTransport.finished(s, c.id, seen)) &&
112
+ t.dependencies.every((d) => FakeRelayTransport.finished(s, d, seen)));
113
+ }
114
+ static summary(s, t) {
115
+ return {
116
+ id: t.id,
117
+ key: t.key,
118
+ title: t.title,
119
+ column: t.column,
120
+ revision: t.revision,
121
+ finished: FakeRelayTransport.finished(s, t.id),
122
+ };
123
+ }
124
+ async identity() {
125
+ await tick();
126
+ return this.read((s) => ({
127
+ provider: this.provider,
128
+ projectId: s.projectId,
129
+ capabilities: [
130
+ 'fake-task-v1',
131
+ ...(this.options.history === false ? [] : ['fake-work-history-v1']),
132
+ ],
133
+ build: { implementation: 'devcontour-harness fake relay (fixture)' },
134
+ }));
135
+ }
136
+ async readTask(ref) {
137
+ await tick();
138
+ const before = this.read((s) => String(s.generation));
139
+ const task = this.read((s) => structuredClone(this.taskOf(s, ref, 'task.read')));
140
+ await this.onPage?.('task', ref);
141
+ await tick();
142
+ const links = this.read((s) => ({
143
+ children: Object.values(s.tasks)
144
+ .filter((x) => x.parentId === task.id)
145
+ .map((x) => FakeRelayTransport.summary(s, x)),
146
+ dependencies: task.dependencies
147
+ .filter((d) => s.tasks[d])
148
+ .map((d) => FakeRelayTransport.summary(s, s.tasks[d])),
149
+ }));
150
+ await this.onPage?.('links', ref);
151
+ await tick();
152
+ const documents = this.read((s) => Object.values(s.documents)
153
+ .filter((d) => d.targets.includes(task.id))
154
+ .map(({ targets, ...d }) => (void targets, d)));
155
+ await this.onPage?.('documents', ref);
156
+ const after = this.read((s) => String(s.generation));
157
+ return { task, ...links, documents, consistency: { before, after } };
158
+ }
159
+ async listComments(ref) {
160
+ await tick();
161
+ return this.read((s) => structuredClone(s.comments[this.taskOf(s, ref, 'comment.list').id] ?? []));
162
+ }
163
+ async addComment(ref, input) {
164
+ await tick();
165
+ if (!input.body.trim())
166
+ this.fail('refused', 'comment.add', 'Пустой комментарий', {
167
+ code: 'INVALID_INPUT',
168
+ mutation: true,
169
+ });
170
+ return this.write('comment.add', (s) => {
171
+ const task = this.taskOf(s, ref, 'comment.add');
172
+ const id = String(++s.seq);
173
+ (s.comments[task.id] ??= []).push({
174
+ id,
175
+ title: input.title,
176
+ body: input.body,
177
+ actor: this.options.actor,
178
+ createdAt: new Date().toISOString(),
179
+ });
180
+ return { commentId: id };
181
+ });
182
+ }
183
+ async setCriterion(ref, input) {
184
+ await tick();
185
+ return this.write('task.criterion', (s) => {
186
+ const task = this.taskOf(s, ref, 'task.criterion');
187
+ if (task.column === 'done')
188
+ this.fail('refused', 'task.criterion', 'Задача в done', {
189
+ code: 'TASK_ACCEPTANCE_LOCKED',
190
+ mutation: true,
191
+ });
192
+ if (task.revision !== input.ifRevision)
193
+ this.fail('conflict', 'task.criterion', 'Ревизия изменилась', {
194
+ code: 'REVISION_CONFLICT',
195
+ actualRevision: task.revision,
196
+ mutation: true,
197
+ });
198
+ const c = task.criteria.find((x) => x.id === input.criterionId);
199
+ if (!c)
200
+ this.fail('not-found', 'task.criterion', 'Критерий не найден', { code: 'NOT_FOUND' });
201
+ c.completed = input.completed;
202
+ task.revision++;
203
+ return { revision: task.revision };
204
+ });
205
+ }
206
+ async move(ref, input) {
207
+ await tick();
208
+ if (!columns.includes(input.column))
209
+ this.fail('refused', 'task.move', 'Колонка', { code: 'INVALID_INPUT' });
210
+ return this.write('task.move', (s) => {
211
+ const task = this.taskOf(s, ref, 'task.move');
212
+ if (task.revision !== input.ifRevision)
213
+ this.fail('conflict', 'task.move', 'Ревизия изменилась', {
214
+ code: 'REVISION_CONFLICT',
215
+ actualRevision: task.revision,
216
+ mutation: true,
217
+ });
218
+ if (input.column === 'done') {
219
+ if (task.criteria.some((c) => !c.completed))
220
+ this.fail('refused', 'task.move', 'Критерии не выполнены', {
221
+ code: 'TASK_ACCEPTANCE_INCOMPLETE',
222
+ mutation: true,
223
+ });
224
+ const children = Object.values(s.tasks).filter((x) => x.parentId === task.id);
225
+ if (children.some((c) => !FakeRelayTransport.finished(s, c.id)) ||
226
+ task.dependencies.some((d) => !FakeRelayTransport.finished(s, d)))
227
+ this.fail('refused', 'task.move', 'Есть блокеры', {
228
+ code: 'TASK_BLOCKED',
229
+ mutation: true,
230
+ });
231
+ }
232
+ task.column = input.column;
233
+ task.revision++;
234
+ return { revision: task.revision };
235
+ });
236
+ }
237
+ /* ---------------------------------------------------------------- */
238
+ /* История */
239
+ /* ---------------------------------------------------------------- */
240
+ requireHistory(operation) {
241
+ if (this.options.history === false)
242
+ this.fail('unsupported', operation, 'История не поддерживается', { code: 'UNSUPPORTED' });
243
+ }
244
+ sessionOf(s, ref, operation) {
245
+ const session = s.sessions[ref] ?? Object.values(s.sessions).find((x) => x.key === ref);
246
+ if (!session)
247
+ this.fail('not-found', operation, 'Сессия не найдена: ' + ref, { code: 'NOT_FOUND' });
248
+ return session;
249
+ }
250
+ view(session) {
251
+ const { runs, openIntents, accepted, ...record } = session;
252
+ void runs;
253
+ void openIntents;
254
+ void accepted;
255
+ return structuredClone(record);
256
+ }
257
+ /** CAS ревизии сессии и fencing по run. */
258
+ guard(session, operation, input, owner = true) {
259
+ if (session.status === 'completed' || session.status === 'cancelled')
260
+ this.fail('conflict', operation, 'Сессия закрыта', {
261
+ code: 'WORK_SESSION_CLOSED',
262
+ mutation: true,
263
+ });
264
+ if (session.revision !== input.ifRevision)
265
+ this.fail('conflict', operation, 'Ревизия сессии изменилась', {
266
+ code: 'REVISION_CONFLICT',
267
+ actualRevision: session.revision,
268
+ mutation: true,
269
+ });
270
+ if (owner &&
271
+ (session.status !== 'open' || !input.runId || input.runId !== session.currentRunId))
272
+ this.fail('conflict', operation, 'Run не владеет сессией', {
273
+ code: 'WORK_RUN_NOT_CURRENT',
274
+ mutation: true,
275
+ });
276
+ }
277
+ newRun(s, session) {
278
+ for (const r of session.runs)
279
+ r.state = 'ended';
280
+ const run = { id: 'run' + ++s.seq, state: 'active' };
281
+ session.runs.push(run);
282
+ session.currentRunId = run.id;
283
+ session.status = 'open';
284
+ return run.id;
285
+ }
286
+ async startSession(input) {
287
+ await tick();
288
+ this.requireHistory('history.start');
289
+ return this.write('history.start', (s) => {
290
+ const id = 's' + ++s.seq;
291
+ const session = {
292
+ id,
293
+ key: 'S-' + id,
294
+ status: 'open',
295
+ revision: 1,
296
+ currentRunId: null,
297
+ parentSessionId: null,
298
+ title: input.title,
299
+ description: input.description,
300
+ result: '',
301
+ runs: [],
302
+ openIntents: [],
303
+ accepted: [],
304
+ };
305
+ const runId = this.newRun(s, session);
306
+ s.sessions[id] = session;
307
+ return { session: this.view(session), runId };
308
+ });
309
+ }
310
+ async getSession(ref) {
311
+ await tick();
312
+ this.requireHistory('history.get');
313
+ return this.read((s) => this.view(this.sessionOf(s, ref, 'history.get')));
314
+ }
315
+ async findSessions(marker) {
316
+ await tick();
317
+ this.requireHistory('history.find');
318
+ return this.read((s) => Object.values(s.sessions)
319
+ .filter((x) => x.title.includes(marker) || x.description.includes(marker))
320
+ .map((x) => this.view(x)));
321
+ }
322
+ async checkpoints(sessionRef) {
323
+ await tick();
324
+ this.requireHistory('history.checkpoints');
325
+ return this.read((s) => {
326
+ const session = this.sessionOf(s, sessionRef, 'history.checkpoints');
327
+ return Object.values(s.checkpoints)
328
+ .filter((c) => c.sessionId === session.id)
329
+ .map(({ closed, children, acceptedSessionIds, ...c }) => (void closed,
330
+ void children,
331
+ void acceptedSessionIds,
332
+ structuredClone(c)));
333
+ });
334
+ }
335
+ async openIntent(sessionRef, input) {
336
+ await tick();
337
+ this.requireHistory('history.intent');
338
+ return this.write('history.intent', (s) => {
339
+ const session = this.sessionOf(s, sessionRef, 'history.intent');
340
+ this.guard(session, 'history.intent', input);
341
+ if (!input.body.trim())
342
+ this.fail('refused', 'history.intent', 'Пустое тело', { code: 'INVALID_INPUT' });
343
+ const id = 'cp' + ++s.seq;
344
+ const checkpoint = {
345
+ id,
346
+ key: 'CP-' + id,
347
+ sessionId: session.id,
348
+ runId: input.runId,
349
+ kind: 'intent',
350
+ intentId: null,
351
+ title: input.title,
352
+ body: input.body,
353
+ closed: false,
354
+ children: [],
355
+ acceptedSessionIds: [],
356
+ };
357
+ s.checkpoints[id] = checkpoint;
358
+ session.openIntents.push(id);
359
+ session.revision++;
360
+ return { session: this.view(session), checkpoint: structuredClone(checkpoint) };
361
+ });
362
+ }
363
+ async checkpoint(intentRef, kind, input) {
364
+ await tick();
365
+ this.requireHistory('history.checkpoint');
366
+ return this.write('history.checkpoint', (s) => {
367
+ const intent = s.checkpoints[intentRef] ?? Object.values(s.checkpoints).find((c) => c.key === intentRef);
368
+ if (!intent || intent.kind !== 'intent')
369
+ this.fail('not-found', 'history.checkpoint', 'Intent не найден', { code: 'NOT_FOUND' });
370
+ const session = s.sessions[intent.sessionId];
371
+ this.guard(session, 'history.checkpoint', input);
372
+ if (intent.closed)
373
+ this.fail('conflict', 'history.checkpoint', 'Intent закрыт', {
374
+ code: 'WORK_INTENT_CLOSED',
375
+ mutation: true,
376
+ });
377
+ if (kind !== 'progress') {
378
+ const accepted = [...intent.acceptedSessionIds, ...(input.acceptedSessionIds ?? [])];
379
+ for (const child of intent.children.map((c) => s.sessions[c])) {
380
+ if (child.status !== 'completed' && child.status !== 'cancelled')
381
+ this.fail('conflict', 'history.checkpoint', 'Дочерние сессии открыты', {
382
+ code: 'WORK_CHILDREN_OPEN',
383
+ mutation: true,
384
+ });
385
+ if (child.status === 'completed' && !accepted.includes(child.id))
386
+ this.fail('conflict', 'history.checkpoint', 'Дочерний результат не принят', {
387
+ code: 'WORK_CHILDREN_UNACCEPTED',
388
+ mutation: true,
389
+ });
390
+ }
391
+ intent.acceptedSessionIds = accepted;
392
+ intent.closed = true;
393
+ session.openIntents = session.openIntents.filter((x) => x !== intent.id);
394
+ }
395
+ const id = 'cp' + ++s.seq;
396
+ const checkpoint = {
397
+ id,
398
+ key: 'CP-' + id,
399
+ sessionId: session.id,
400
+ runId: input.runId,
401
+ kind,
402
+ intentId: intent.id,
403
+ title: input.title,
404
+ body: input.body,
405
+ closed: true,
406
+ children: [],
407
+ acceptedSessionIds: input.acceptedSessionIds ?? [],
408
+ };
409
+ s.checkpoints[id] = checkpoint;
410
+ session.revision++;
411
+ return { session: this.view(session), checkpoint: structuredClone(checkpoint) };
412
+ });
413
+ }
414
+ async fork(intentRef, input) {
415
+ await tick();
416
+ this.requireHistory('history.fork');
417
+ return this.write('history.fork', (s) => {
418
+ const intent = s.checkpoints[intentRef] ?? Object.values(s.checkpoints).find((c) => c.key === intentRef);
419
+ if (!intent || intent.kind !== 'intent' || intent.closed)
420
+ this.fail('conflict', 'history.fork', 'Нужен открытый intent', {
421
+ code: 'WORK_INTENT_REQUIRED',
422
+ mutation: true,
423
+ });
424
+ const parent = s.sessions[intent.sessionId];
425
+ this.guard(parent, 'history.fork', input);
426
+ const id = 's' + ++s.seq;
427
+ const child = {
428
+ id,
429
+ key: 'S-' + id,
430
+ status: 'pending',
431
+ revision: 1,
432
+ currentRunId: null,
433
+ parentSessionId: parent.id,
434
+ title: input.title,
435
+ description: input.description,
436
+ result: '',
437
+ runs: [],
438
+ openIntents: [],
439
+ accepted: [],
440
+ };
441
+ s.sessions[id] = child;
442
+ intent.children.push(id);
443
+ parent.revision++;
444
+ return { session: this.view(parent), childSession: this.view(child) };
445
+ });
446
+ }
447
+ async attach(sessionRef, input) {
448
+ await tick();
449
+ this.requireHistory('history.attach');
450
+ return this.write('history.attach', (s) => {
451
+ const session = this.sessionOf(s, sessionRef, 'history.attach');
452
+ this.guard(session, 'history.attach', input, false);
453
+ if (session.status !== 'pending')
454
+ this.fail('conflict', 'history.attach', 'Нужно состояние pending', {
455
+ code: 'WORK_SESSION_STATE',
456
+ mutation: true,
457
+ });
458
+ const runId = this.newRun(s, session);
459
+ session.revision++;
460
+ return { session: this.view(session), runId };
461
+ });
462
+ }
463
+ async takeover(sessionRef, input) {
464
+ await tick();
465
+ this.requireHistory('history.takeover');
466
+ return this.write('history.takeover', (s) => {
467
+ const session = this.sessionOf(s, sessionRef, 'history.takeover');
468
+ this.guard(session, 'history.takeover', input, false);
469
+ if (session.status !== 'open')
470
+ this.fail('conflict', 'history.takeover', 'Нужно состояние open', {
471
+ code: 'WORK_SESSION_STATE',
472
+ mutation: true,
473
+ });
474
+ const runId = this.newRun(s, session);
475
+ session.revision++;
476
+ return { session: this.view(session), runId };
477
+ });
478
+ }
479
+ async finishSession(sessionRef, outcome, input) {
480
+ await tick();
481
+ this.requireHistory('history.finish');
482
+ return this.write('history.finish', (s) => {
483
+ const session = this.sessionOf(s, sessionRef, 'history.finish');
484
+ const owner = session.status === 'open' || outcome === 'complete';
485
+ if (!owner && input.runId)
486
+ this.fail('conflict', 'history.finish', 'runId только для open', {
487
+ code: 'WORK_RUN_NOT_CURRENT',
488
+ mutation: true,
489
+ });
490
+ this.guard(session, 'history.finish', input, owner);
491
+ if (session.openIntents.length)
492
+ this.fail('conflict', 'history.finish', 'Есть открытые intent', {
493
+ code: 'WORK_INTENTS_OPEN',
494
+ mutation: true,
495
+ });
496
+ session.status = outcome === 'complete' ? 'completed' : 'cancelled';
497
+ session.result = input.result;
498
+ session.currentRunId = null;
499
+ for (const r of session.runs)
500
+ r.state = 'ended';
501
+ session.revision++;
502
+ return { session: this.view(session) };
503
+ });
504
+ }
505
+ }
506
+ /**
507
+ * Действия «другого участника» прямо в подделке: постановка задач, правки,
508
+ * перехват сессии. Ядро коннектора ими не пользуется.
509
+ */
510
+ export class FakeAdmin {
511
+ backend;
512
+ constructor(backend) {
513
+ this.backend = backend;
514
+ }
515
+ write(fn) {
516
+ return this.backend.with((s) => {
517
+ const out = fn(s);
518
+ s.generation++;
519
+ return out;
520
+ }, true);
521
+ }
522
+ createTask(input) {
523
+ return this.write((s) => {
524
+ const n = Object.keys(s.tasks).length + 1;
525
+ const id = 't' + n;
526
+ s.tasks[id] = {
527
+ id,
528
+ key: 'PRODUCT-' + n,
529
+ title: input.title,
530
+ description: input.description,
531
+ column: input.column ?? 'ready',
532
+ revision: 1,
533
+ parentId: input.parentId ?? null,
534
+ dependencies: input.dependencies ?? [],
535
+ criteria: (input.criteria ?? []).map((c, i) => ({
536
+ id: `${id}c${i + 1}`,
537
+ title: c.title,
538
+ summary: c.summary ?? '',
539
+ description: '',
540
+ completed: false,
541
+ })),
542
+ };
543
+ return { id, key: s.tasks[id].key, criteria: s.tasks[id].criteria.map((c) => c.id) };
544
+ });
545
+ }
546
+ task(id) {
547
+ return this.backend.with((s) => structuredClone(s.tasks[id]), false);
548
+ }
549
+ updateTask(id, patch) {
550
+ this.write((s) => {
551
+ Object.assign(s.tasks[id], patch);
552
+ s.tasks[id].revision++;
553
+ });
554
+ }
555
+ addCriterion(id, title) {
556
+ return this.write((s) => {
557
+ const task = s.tasks[id];
558
+ const cid = `${id}c${task.criteria.length + 1}`;
559
+ task.criteria.push({ id: cid, title, summary: '', description: '', completed: false });
560
+ task.revision++;
561
+ return cid;
562
+ });
563
+ }
564
+ updateCriterion(id, criterionId, title) {
565
+ this.write((s) => {
566
+ const c = s.tasks[id].criteria.find((x) => x.id === criterionId);
567
+ c.title = title;
568
+ c.completed = false;
569
+ s.tasks[id].revision++;
570
+ });
571
+ }
572
+ completeCriterion(id, criterionId, completed = true) {
573
+ this.write((s) => {
574
+ s.tasks[id].criteria.find((x) => x.id === criterionId).completed = completed;
575
+ s.tasks[id].revision++;
576
+ });
577
+ }
578
+ addDocument(taskId, doc) {
579
+ return this.write((s) => {
580
+ const n = Object.keys(s.documents).length + 1;
581
+ const id = 'd' + n;
582
+ s.documents[id] = {
583
+ id,
584
+ key: 'DOC-' + n,
585
+ name: doc.name,
586
+ body: doc.body,
587
+ revision: 1,
588
+ targets: [taskId],
589
+ };
590
+ return { id };
591
+ });
592
+ }
593
+ updateDocument(id, body) {
594
+ this.write((s) => {
595
+ s.documents[id].body = body;
596
+ s.documents[id].revision++;
597
+ });
598
+ }
599
+ addDependency(id, dependsOn) {
600
+ this.write((s) => {
601
+ s.tasks[id].dependencies.push(dependsOn);
602
+ s.tasks[id].revision++;
603
+ });
604
+ }
605
+ /** Перемещение с соблюдением done guards (как у человека в backend). */
606
+ move(id, column) {
607
+ this.write((s) => {
608
+ const task = s.tasks[id];
609
+ if (column === 'done' &&
610
+ (task.criteria.some((c) => !c.completed) ||
611
+ Object.values(s.tasks).some((x) => x.parentId === id && !FakeRelayTransport.finished(s, x.id)) ||
612
+ task.dependencies.some((d) => !FakeRelayTransport.finished(s, d))))
613
+ throw new Error('done guard: задача не может быть завершена');
614
+ task.column = column;
615
+ task.revision++;
616
+ });
617
+ }
618
+ comment(id, body, actor = 'someone-else') {
619
+ this.write((s) => {
620
+ (s.comments[id] ??= []).push({
621
+ id: String(++s.seq),
622
+ title: 'note',
623
+ body,
624
+ actor,
625
+ createdAt: new Date().toISOString(),
626
+ });
627
+ });
628
+ }
629
+ takeover(sessionId) {
630
+ this.write((s) => {
631
+ const session = s.sessions[sessionId];
632
+ for (const r of session.runs)
633
+ r.state = 'ended';
634
+ const run = { id: 'run' + ++s.seq, state: 'active' };
635
+ session.runs.push(run);
636
+ session.currentRunId = run.id;
637
+ session.revision++;
638
+ });
639
+ }
640
+ state() {
641
+ return this.backend.with((s) => structuredClone(s), false);
642
+ }
643
+ }
644
+ /** Барьер одного процесса. */
645
+ export class LocalBarrier {
646
+ arrived;
647
+ released;
648
+ reached = new Promise((r) => (this.arrived = r));
649
+ gate = new Promise((r) => (this.released = r));
650
+ async arrive() {
651
+ this.arrived();
652
+ await this.gate;
653
+ }
654
+ release() {
655
+ this.released();
656
+ }
657
+ }
658
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
659
+ /** Барьер между процессами: файлы `<name>.reached` и `<name>.release` в общем каталоге. */
660
+ export class FileBarrier {
661
+ dir;
662
+ name;
663
+ timeoutMs;
664
+ constructor(dir, name, timeoutMs = 30_000) {
665
+ this.dir = dir;
666
+ this.name = name;
667
+ this.timeoutMs = timeoutMs;
668
+ }
669
+ file(kind) {
670
+ return join(this.dir, `${this.name}.${kind}`);
671
+ }
672
+ async arrive() {
673
+ mkdirSync(this.dir, { recursive: true });
674
+ writeFileSync(this.file('reached'), String(process.pid));
675
+ const until = Date.now() + this.timeoutMs;
676
+ while (!existsSync(this.file('release'))) {
677
+ if (Date.now() > until)
678
+ throw new Error(`Барьер ${this.name}: нет разрешения за ${this.timeoutMs} мс`);
679
+ await sleep(20);
680
+ }
681
+ }
682
+ /** Ждать прибытия (конечный срок). */
683
+ async reached(timeoutMs = this.timeoutMs) {
684
+ const until = Date.now() + timeoutMs;
685
+ while (!existsSync(this.file('reached'))) {
686
+ if (Date.now() > until)
687
+ throw new Error(`Барьер ${this.name}: никто не прибыл за ${timeoutMs} мс`);
688
+ await sleep(20);
689
+ }
690
+ return Number(readFileSync(this.file('reached'), 'utf8'));
691
+ }
692
+ release() {
693
+ mkdirSync(this.dir, { recursive: true });
694
+ writeFileSync(this.file('release'), '1');
695
+ }
696
+ }
697
+ const mutating = new Set([
698
+ 'addComment',
699
+ 'setCriterion',
700
+ 'move',
701
+ 'startSession',
702
+ 'openIntent',
703
+ 'checkpoint',
704
+ 'fork',
705
+ 'attach',
706
+ 'takeover',
707
+ 'finishSession',
708
+ ]);
709
+ /**
710
+ * Обёртка отказов вокруг **любого** транспорта (подделки или настоящего
711
+ * адаптера): управляемые точки отказа без случайных задержек.
712
+ */
713
+ export function faultyTransport(inner, rules) {
714
+ const calls = {};
715
+ const wrap = (op) => async (...args) => {
716
+ const n = (calls[op] = (calls[op] ?? 0) + 1);
717
+ const rule = rules.find((r) => r.op === op && (r.nth === undefined || (n >= r.nth && n < r.nth + (r.times ?? 1))));
718
+ const call = () => inner[op].apply(inner, args);
719
+ const error = (kind, message) => new TransportError(kind, String(op), 'injected: ' + (rule?.message ?? message), {
720
+ provider: inner.provider,
721
+ mutation: mutating.has(op),
722
+ });
723
+ if (!rule)
724
+ return call();
725
+ switch (rule.action) {
726
+ case 'refuse': {
727
+ const refusal = rule.refusal ?? { kind: 'refused', code: 'VALIDATION_ERROR' };
728
+ throw new TransportError(refusal.kind, String(op), 'injected: ' + (rule.message ?? 'отказ получателя'), {
729
+ provider: inner.provider,
730
+ code: refusal.code,
731
+ mutation: mutating.has(op),
732
+ });
733
+ }
734
+ case 'fail-before-send':
735
+ throw error('unavailable', 'запрос не отправлен');
736
+ case 'timeout':
737
+ throw error('uncertain', 'таймаут, исход неизвестен');
738
+ case 'drop-response':
739
+ await call();
740
+ // Чужая запись между фиксацией у получателя и read-back (гонка).
741
+ await rule.after?.();
742
+ throw error('uncertain', 'ответ потерян после выполнения');
743
+ case 'protocol-after':
744
+ await call();
745
+ throw error('protocol', 'ответ вне договора');
746
+ case 'barrier':
747
+ await rule.barrier.arrive();
748
+ return call();
749
+ case 'exit-before':
750
+ process.exit(rule.exitCode ?? 75);
751
+ break;
752
+ case 'exit-after': {
753
+ await call();
754
+ process.exit(rule.exitCode ?? 75);
755
+ break;
756
+ }
757
+ case 'after': {
758
+ const out = await call();
759
+ await rule.after?.();
760
+ return out;
761
+ }
762
+ }
763
+ return call();
764
+ };
765
+ const proxy = {
766
+ provider: inner.provider,
767
+ calls,
768
+ identity: wrap('identity'),
769
+ readTask: wrap('readTask'),
770
+ listComments: wrap('listComments'),
771
+ addComment: wrap('addComment'),
772
+ setCriterion: wrap('setCriterion'),
773
+ move: wrap('move'),
774
+ startSession: wrap('startSession'),
775
+ getSession: wrap('getSession'),
776
+ findSessions: wrap('findSessions'),
777
+ checkpoints: wrap('checkpoints'),
778
+ openIntent: wrap('openIntent'),
779
+ checkpoint: wrap('checkpoint'),
780
+ fork: wrap('fork'),
781
+ attach: wrap('attach'),
782
+ takeover: wrap('takeover'),
783
+ finishSession: wrap('finishSession'),
784
+ };
785
+ return proxy;
786
+ }