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

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 (258) hide show
  1. package/README.md +24 -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/review-probes.md +120 -0
  10. package/docs/review-protocol.md +251 -0
  11. package/docs/toolchains.md +167 -0
  12. package/docs/turbo-starter.md +269 -0
  13. package/docs/upgrade.md +48 -12
  14. package/examples/README.md +1 -0
  15. package/examples/toolchains/README.md +32 -0
  16. package/examples/toolchains/go/bad/label/label.go +9 -0
  17. package/examples/toolchains/go/good/label/label.go +9 -0
  18. package/examples/toolchains/go/policy.json +79 -0
  19. package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
  20. package/examples/toolchains/go/repo/_gitignore +1 -0
  21. package/examples/toolchains/go/repo/go.mod +5 -0
  22. package/examples/toolchains/go/repo/go.sum +2 -0
  23. package/examples/toolchains/go/repo/label/label.go +9 -0
  24. package/examples/toolchains/go/repo/label/label_test.go +27 -0
  25. package/examples/toolchains/go/repo/label/version.go +9 -0
  26. package/examples/toolchains/go/task.json +25 -0
  27. package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
  28. package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
  29. package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
  30. package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
  31. package/examples/toolchains/mixed-go/policy.json +126 -0
  32. package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
  33. package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
  34. package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
  35. package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
  36. package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
  37. package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
  38. package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
  39. package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
  40. package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
  41. package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
  42. package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
  43. package/examples/toolchains/mixed-go/task.json +25 -0
  44. package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
  45. package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
  46. package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
  47. package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
  48. package/examples/toolchains/mixed-python/policy.json +121 -0
  49. package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
  50. package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
  51. package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
  52. package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
  53. package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
  54. package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
  55. package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
  56. package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
  57. package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
  58. package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
  59. package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
  60. package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
  61. package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
  62. package/examples/toolchains/mixed-python/task.json +25 -0
  63. package/examples/toolchains/npm/bad/src/label.js +4 -0
  64. package/examples/toolchains/npm/good/src/label.js +4 -0
  65. package/examples/toolchains/npm/policy.json +82 -0
  66. package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
  67. package/examples/toolchains/npm/repo/_gitignore +4 -0
  68. package/examples/toolchains/npm/repo/package-lock.json +22 -0
  69. package/examples/toolchains/npm/repo/package.json +13 -0
  70. package/examples/toolchains/npm/repo/src/label.js +4 -0
  71. package/examples/toolchains/npm/repo/test/label.test.js +13 -0
  72. package/examples/toolchains/npm/task.json +25 -0
  73. package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
  74. package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
  75. package/examples/toolchains/pnpm/policy.json +85 -0
  76. package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
  77. package/examples/toolchains/pnpm/repo/_gitignore +4 -0
  78. package/examples/toolchains/pnpm/repo/package.json +10 -0
  79. package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
  80. package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
  81. package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
  82. package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
  83. package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
  84. package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
  85. package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
  86. package/examples/toolchains/pnpm/task.json +25 -0
  87. package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
  88. package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
  89. package/examples/toolchains/python/policy.json +80 -0
  90. package/examples/toolchains/python/repo/.python-version +1 -0
  91. package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
  92. package/examples/toolchains/python/repo/_gitignore +5 -0
  93. package/examples/toolchains/python/repo/pyproject.toml +19 -0
  94. package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
  95. package/examples/toolchains/python/repo/tests/test_label.py +13 -0
  96. package/examples/toolchains/python/repo/uv.lock +108 -0
  97. package/examples/toolchains/python/task.json +25 -0
  98. package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
  99. package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
  100. package/examples/toolchains/turbo/policy.json +110 -0
  101. package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
  102. package/examples/toolchains/turbo/repo/_gitignore +4 -0
  103. package/examples/toolchains/turbo/repo/package.json +13 -0
  104. package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
  105. package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
  106. package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
  107. package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
  108. package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
  109. package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
  110. package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
  111. package/examples/toolchains/turbo/repo/turbo.json +7 -0
  112. package/examples/toolchains/turbo/task.json +25 -0
  113. package/examples/turbo-starter/README.md +16 -0
  114. package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
  115. package/examples/turbo-starter/changes/issued-at.patch +130 -0
  116. package/examples/turbo-starter/cleanup.mjs +58 -0
  117. package/examples/turbo-starter/graph.json +32 -0
  118. package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
  119. package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
  120. package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
  121. package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
  122. package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
  123. package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
  124. package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
  125. package/examples/turbo-starter/harness/env/backend.env.template +7 -0
  126. package/examples/turbo-starter/harness/env/compose.env.template +6 -0
  127. package/examples/turbo-starter/policy.json +109 -0
  128. package/examples/turbo-starter/policy.lifecycle.json +114 -0
  129. package/examples/turbo-starter/setup.mjs +352 -0
  130. package/lib/agents/cli-adapter.js +2 -0
  131. package/lib/agents/review-probe.d.ts +179 -7
  132. package/lib/agents/review-probe.js +648 -55
  133. package/lib/agents/schemas.d.ts +3 -1
  134. package/lib/agents/schemas.js +7 -3
  135. package/lib/cli.js +167 -7
  136. package/lib/contracts/cli.d.ts +820 -11
  137. package/lib/contracts/cli.js +243 -4
  138. package/lib/contracts/errors.d.ts +3 -1
  139. package/lib/contracts/errors.js +2 -0
  140. package/lib/contracts/events.d.ts +37 -5
  141. package/lib/contracts/events.js +9 -0
  142. package/lib/contracts/execution.d.ts +176 -8
  143. package/lib/contracts/execution.js +9 -1
  144. package/lib/contracts/failure.js +5 -0
  145. package/lib/contracts/index.d.ts +1 -0
  146. package/lib/contracts/index.js +1 -0
  147. package/lib/contracts/policy.d.ts +15 -0
  148. package/lib/contracts/policy.js +10 -0
  149. package/lib/contracts/ports.d.ts +50 -0
  150. package/lib/contracts/profiles.d.ts +14 -0
  151. package/lib/contracts/profiles.js +4 -0
  152. package/lib/contracts/published.d.ts +230 -7
  153. package/lib/contracts/result.d.ts +171 -0
  154. package/lib/contracts/result.js +39 -1
  155. package/lib/contracts/review.d.ts +231 -0
  156. package/lib/contracts/review.js +326 -2
  157. package/lib/contracts/toolchain.d.ts +96 -0
  158. package/lib/contracts/toolchain.js +89 -0
  159. package/lib/control/doctor.d.ts +16 -1
  160. package/lib/control/doctor.js +45 -3
  161. package/lib/control/harness.d.ts +52 -6
  162. package/lib/control/harness.js +28 -1
  163. package/lib/control/recovery.d.ts +3 -1
  164. package/lib/control/review.js +14 -6
  165. package/lib/engine/classify.js +1 -0
  166. package/lib/engine/engine.d.ts +39 -15
  167. package/lib/engine/prompt.d.ts +29 -1
  168. package/lib/engine/prompt.js +57 -0
  169. package/lib/engine/property-recovery.js +2 -0
  170. package/lib/engine/result.js +52 -0
  171. package/lib/engine/reuse.js +6 -1
  172. package/lib/engine/runner.d.ts +25 -0
  173. package/lib/engine/runner.js +491 -122
  174. package/lib/environment/lifecycle.js +6 -0
  175. package/lib/environment/live-calls.js +3 -2
  176. package/lib/environment/os-tools.d.ts +16 -0
  177. package/lib/environment/os-tools.js +35 -0
  178. package/lib/environment/process.js +5 -4
  179. package/lib/integrations/connector/commands.d.ts +358 -0
  180. package/lib/integrations/connector/commands.js +428 -0
  181. package/lib/integrations/connector/config.d.ts +215 -0
  182. package/lib/integrations/connector/config.js +214 -0
  183. package/lib/integrations/connector/delivery.d.ts +107 -0
  184. package/lib/integrations/connector/delivery.js +591 -0
  185. package/lib/integrations/connector/fake.d.ts +291 -0
  186. package/lib/integrations/connector/fake.js +786 -0
  187. package/lib/integrations/connector/history.d.ts +124 -0
  188. package/lib/integrations/connector/history.js +497 -0
  189. package/lib/integrations/connector/index.d.ts +16 -0
  190. package/lib/integrations/connector/index.js +16 -0
  191. package/lib/integrations/connector/redact.d.ts +19 -0
  192. package/lib/integrations/connector/redact.js +91 -0
  193. package/lib/integrations/connector/registry.d.ts +15 -0
  194. package/lib/integrations/connector/registry.js +43 -0
  195. package/lib/integrations/connector/snapshot.d.ts +131 -0
  196. package/lib/integrations/connector/snapshot.js +422 -0
  197. package/lib/integrations/connector/source.d.ts +37 -0
  198. package/lib/integrations/connector/source.js +73 -0
  199. package/lib/integrations/connector/steps.d.ts +71 -0
  200. package/lib/integrations/connector/steps.js +188 -0
  201. package/lib/integrations/connector/store.d.ts +140 -0
  202. package/lib/integrations/connector/store.js +277 -0
  203. package/lib/integrations/connector/summary.d.ts +38 -0
  204. package/lib/integrations/connector/summary.js +59 -0
  205. package/lib/integrations/connector/testing-worker.d.ts +1 -0
  206. package/lib/integrations/connector/testing-worker.js +81 -0
  207. package/lib/integrations/connector/testing.d.ts +205 -0
  208. package/lib/integrations/connector/testing.js +1615 -0
  209. package/lib/integrations/connector/transport.d.ts +240 -0
  210. package/lib/integrations/connector/transport.js +28 -0
  211. package/lib/integrations/memory/context.d.ts +120 -0
  212. package/lib/integrations/memory/context.js +239 -0
  213. package/lib/integrations/memory/index.d.ts +8 -0
  214. package/lib/integrations/memory/index.js +8 -0
  215. package/lib/integrations/memory/provider.d.ts +79 -0
  216. package/lib/integrations/memory/provider.js +22 -0
  217. package/lib/integrations/memory/sink.d.ts +56 -0
  218. package/lib/integrations/memory/sink.js +116 -0
  219. package/lib/integrations/memory/testing.d.ts +44 -0
  220. package/lib/integrations/memory/testing.js +61 -0
  221. package/lib/integrations/relay-v1/index.d.ts +122 -0
  222. package/lib/integrations/relay-v1/index.js +860 -0
  223. package/lib/integrations/relay-v2/index.d.ts +145 -0
  224. package/lib/integrations/relay-v2/index.js +752 -0
  225. package/lib/integrations/relay-v2/process.d.ts +32 -0
  226. package/lib/integrations/relay-v2/process.js +112 -0
  227. package/lib/integrations/relay-v2/protocol.d.ts +172 -0
  228. package/lib/integrations/relay-v2/protocol.js +142 -0
  229. package/lib/profiles/resolve.d.ts +5 -1
  230. package/lib/profiles/resolve.js +31 -0
  231. package/lib/storage/document.d.ts +60 -12
  232. package/lib/toolchains/index.d.ts +9 -0
  233. package/lib/toolchains/index.js +9 -0
  234. package/lib/toolchains/inspect.d.ts +105 -0
  235. package/lib/toolchains/inspect.js +831 -0
  236. package/lib/toolchains/manifests.d.ts +67 -0
  237. package/lib/toolchains/manifests.js +238 -0
  238. package/lib/toolchains/probe.d.ts +28 -0
  239. package/lib/toolchains/probe.js +54 -0
  240. package/lib/toolchains/recipes.d.ts +33 -0
  241. package/lib/toolchains/recipes.js +51 -0
  242. package/lib/verification/acceptance.d.ts +16 -0
  243. package/lib/verification/acceptance.js +16 -0
  244. package/lib/verification/gates.d.ts +16 -0
  245. package/lib/verification/gates.js +44 -3
  246. package/package.json +22 -1
  247. package/profiles/go-gotestsum.json +21 -0
  248. package/profiles/npm-ci.json +22 -0
  249. package/profiles/pnpm-frozen.json +24 -0
  250. package/profiles/python-uv.json +26 -0
  251. package/schemas/execution-document.schema.json +155 -0
  252. package/schemas/execution-policy.schema.json +48 -0
  253. package/schemas/result-envelope.schema.json +213 -1
  254. package/schemas/run-event.schema.json +93 -0
  255. package/schemas/run-result.schema.json +207 -0
  256. package/schemas/stack-profile.schema.json +43 -0
  257. package/templates/controller-mode/AGENTS.controller.md +58 -0
  258. package/templates/controller-mode/README.md +44 -0
@@ -0,0 +1,188 @@
1
+ import { TransportError } from "./transport.js";
2
+ export class StepUncertain extends Error {
3
+ step;
4
+ name = 'StepUncertain';
5
+ constructor(step, message) {
6
+ super(message);
7
+ this.step = step;
8
+ }
9
+ }
10
+ export class StepConflict extends Error {
11
+ step;
12
+ currentRevision;
13
+ name = 'StepConflict';
14
+ constructor(step, message, currentRevision) {
15
+ super(message);
16
+ this.step = step;
17
+ this.currentRevision = currentRevision;
18
+ }
19
+ }
20
+ /** Отправка точно не состоялась (или доказано отсутствие записи): шаг можно повторить позже. */
21
+ export class StepRetry extends Error {
22
+ step;
23
+ name = 'StepRetry';
24
+ constructor(step, message) {
25
+ super(message);
26
+ this.step = step;
27
+ }
28
+ }
29
+ const at = () => new Date().toISOString();
30
+ const reasonOf = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 1000);
31
+ /**
32
+ * Выполнить шаг по протоколу. `prepare` — входы, известные только в момент
33
+ * отправки (CAS-ревизия), записываются вместе с `sending` до вызова.
34
+ */
35
+ export async function runStep(io, handlers) {
36
+ let step = io.load();
37
+ if (step.status === 'done' || step.status === 'skipped')
38
+ return step.result ?? {};
39
+ if (step.status === 'uncertain')
40
+ throw new StepUncertain(step.n, step.reason ?? 'исход шага не доказан');
41
+ if (step.status === 'conflict')
42
+ throw new StepConflict(step.n, step.reason ?? 'конфликт');
43
+ const settle = (rb, context) => {
44
+ switch (rb.state) {
45
+ case 'found':
46
+ io.save({ ...step, status: 'done', result: rb.result, reason: context, updatedAt: at() });
47
+ return rb.result;
48
+ case 'absent':
49
+ // Запись не применена: шаг снова planned; повтор — следующим явным вызовом.
50
+ io.save({
51
+ ...step,
52
+ status: 'planned',
53
+ reason: `${context}: ${rb.reason}`,
54
+ updatedAt: at(),
55
+ });
56
+ throw new StepRetry(step.n, `${context}: ${rb.reason}`);
57
+ case 'foreign':
58
+ io.save({
59
+ ...step,
60
+ status: 'conflict',
61
+ reason: `${context}: ${rb.reason}`,
62
+ updatedAt: at(),
63
+ });
64
+ throw new StepConflict(step.n, `${context}: ${rb.reason}`);
65
+ case 'ambiguous':
66
+ io.save({
67
+ ...step,
68
+ status: 'uncertain',
69
+ reason: `${context}: ${rb.reason}`,
70
+ updatedAt: at(),
71
+ });
72
+ throw new StepUncertain(step.n, `${context}: ${rb.reason}`);
73
+ }
74
+ };
75
+ const readBack = async (context) => {
76
+ let rb;
77
+ try {
78
+ rb = await handlers.readBack(step.input);
79
+ }
80
+ catch (error) {
81
+ rb = { state: 'ambiguous', reason: 'read-back недоступен: ' + reasonOf(error) };
82
+ }
83
+ return settle(rb, context);
84
+ };
85
+ if (step.status === 'sending') {
86
+ // Прежний процесс оборвался во время вызова: только факты получателя.
87
+ try {
88
+ return await readBack('после прерванной отправки');
89
+ }
90
+ catch (error) {
91
+ if (!(error instanceof StepRetry))
92
+ throw error;
93
+ step = io.load();
94
+ }
95
+ }
96
+ const refused = (error) => {
97
+ const reason = `подтверждённый отказ получателя (${error.details.code ?? error.kind}): ${reasonOf(error)}`;
98
+ io.save({ ...step, status: 'planned', reason, updatedAt: at() });
99
+ throw new StepRetry(step.n, reason);
100
+ };
101
+ const extra = handlers.prepare?.() ?? {};
102
+ step = {
103
+ ...step,
104
+ status: 'sending',
105
+ input: { ...step.input, ...extra },
106
+ sends: step.sends + 1,
107
+ updatedAt: at(),
108
+ };
109
+ io.save(step);
110
+ let result;
111
+ try {
112
+ result = await handlers.send(step.input);
113
+ }
114
+ catch (error) {
115
+ // Составной шаг (история) сам разобрал свои записи и назвал итог.
116
+ if (error instanceof StepConflict) {
117
+ io.save({ ...step, status: 'conflict', reason: error.message, updatedAt: at() });
118
+ throw new StepConflict(step.n, error.message, error.currentRevision);
119
+ }
120
+ if (error instanceof StepUncertain) {
121
+ io.save({ ...step, status: 'uncertain', reason: error.message, updatedAt: at() });
122
+ throw new StepUncertain(step.n, error.message);
123
+ }
124
+ if (error instanceof StepRetry) {
125
+ io.save({ ...step, status: 'planned', reason: error.message, updatedAt: at() });
126
+ throw new StepRetry(step.n, error.message);
127
+ }
128
+ if (!(error instanceof TransportError)) {
129
+ // Не транспорт (ошибка кода/отмена): исход вызова неизвестен.
130
+ return readBack('исключение во время вызова: ' + reasonOf(error));
131
+ }
132
+ switch (error.kind) {
133
+ case 'unavailable':
134
+ io.save({ ...step, status: 'planned', reason: reasonOf(error), updatedAt: at() });
135
+ throw new StepRetry(step.n, 'получатель недоступен, запрос не отправлен: ' + reasonOf(error));
136
+ case 'unsupported':
137
+ io.save({ ...step, status: 'planned', reason: reasonOf(error), updatedAt: at() });
138
+ throw new StepRetry(step.n, 'операция не поддерживается: ' + reasonOf(error));
139
+ case 'conflict': {
140
+ // Запись без CAS (комментарий): получатель её подтверждённо отклонил —
141
+ // записи нет, повтор безопасен (тот же маркер).
142
+ if (handlers.appendOnly)
143
+ return refused(error);
144
+ // Отказ CAS/состояния: своё ли это изменение — решают только факты.
145
+ // Ответ conflict означает, что получатель запись отклонил: «нет
146
+ // записи» здесь — конфликт (WORK_*, REVISION_CONFLICT), а не повод
147
+ // повторить старую команду по новой ревизии.
148
+ const code = error.details.code ?? 'CONFLICT';
149
+ let rb;
150
+ try {
151
+ rb = await handlers.readBack(step.input);
152
+ }
153
+ catch (failure) {
154
+ rb = { state: 'ambiguous', reason: 'read-back недоступен: ' + reasonOf(failure) };
155
+ }
156
+ if (rb.state === 'absent')
157
+ rb = { state: 'foreign', reason: `${code}: ${reasonOf(error)}` };
158
+ return settle(rb, 'конфликт ' + code);
159
+ }
160
+ case 'refused':
161
+ // Подтверждённый отказ (структурный ответ получателя): мутации не было.
162
+ // Это не неизвестный исход: шаг снова planned, повтор — следующей доставкой.
163
+ return refused(error);
164
+ case 'not-found':
165
+ io.save({ ...step, status: 'conflict', reason: reasonOf(error), updatedAt: at() });
166
+ throw new StepConflict(step.n, `запись получателя не найдена (${error.details.code ?? error.kind}): ${reasonOf(error)}`, 'removed');
167
+ case 'protocol':
168
+ // Ответ вне договора на чтение/немутирующий вызов — не неизвестный исход записи.
169
+ if (error.details.mutation === false)
170
+ return refused(error);
171
+ return readBack('неизвестный исход (' + error.kind + ')');
172
+ case 'uncertain':
173
+ return readBack('неизвестный исход (' + error.kind + ')');
174
+ }
175
+ }
176
+ io.save({ ...step, status: 'done', result, updatedAt: at() });
177
+ return result;
178
+ }
179
+ export const markerOf = (key, n) => `<!-- devcontour-harness:op=${key}:step=${n} -->`;
180
+ export const newStep = (n, kind, key, input) => ({
181
+ n,
182
+ kind,
183
+ status: 'planned',
184
+ marker: markerOf(key, n),
185
+ input,
186
+ sends: 0,
187
+ updatedAt: at(),
188
+ });
@@ -0,0 +1,140 @@
1
+ import { HarnessError } from '../../contracts/index.ts';
2
+ import type { ProviderId } from './transport.ts';
3
+ export type StepStatus = 'planned' | 'sending' | 'done' | 'skipped' | 'conflict' | 'uncertain';
4
+ /** Шаг доставки или истории: внешний вызов с маркером и его подтверждённый итог. */
5
+ export interface StepRecord {
6
+ n: number;
7
+ kind: 'comment' | 'historical-comment' | 'criterion' | 'move' | 'history' | 'session.start' | 'session.intent' | 'session.fork' | 'session.attach' | 'session.checkpoint' | 'session.finish';
8
+ status: StepStatus;
9
+ marker: string;
10
+ /** Параметры шага, закреплённые при планировании (критерий, колонка, выход истории). */
11
+ input: Record<string, unknown>;
12
+ /** Подтверждённый итог (ID комментария, новая ревизия, ID сессии). */
13
+ result?: Record<string, unknown>;
14
+ reason?: string;
15
+ /** Сколько раз шаг отправлялся (вне `planned`); не повод для повтора. */
16
+ sends: number;
17
+ updatedAt: string;
18
+ }
19
+ export type OperationStatus = 'intent' | 'in-progress' | 'delivered' | 'conflict' | 'uncertain' | 'failed';
20
+ export interface OperationRecord {
21
+ key: string;
22
+ provider: ProviderId;
23
+ projectId: string;
24
+ workId: string;
25
+ runId: string;
26
+ taskId: string;
27
+ resultDigest: string;
28
+ /** Редакция (semantic digest) постановки, к которой относится результат. */
29
+ revision: string;
30
+ outcome: string;
31
+ status: OperationStatus;
32
+ steps: StepRecord[];
33
+ /** Шаги спланированы (после предварительной сверки, до первой мутации). */
34
+ planned: boolean;
35
+ reason?: string;
36
+ /** Текущая редакция источника при конфликте. */
37
+ currentRevision?: string;
38
+ receipt?: Record<string, unknown>;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ }
42
+ export interface LeaseRecord {
43
+ key: string;
44
+ token: string;
45
+ owner: string;
46
+ expiresAt: number;
47
+ fence: number;
48
+ }
49
+ export interface SnapshotRecord {
50
+ revision: string;
51
+ graphId: string;
52
+ graph: unknown;
53
+ context: unknown;
54
+ manifest: unknown;
55
+ createdAt: string;
56
+ }
57
+ /** Доверенный receipt доставки принятого результата: основание предпосылки зависимой задачи. */
58
+ export interface ReceiptRecord {
59
+ issuer: string;
60
+ orderId: string;
61
+ taskId: string;
62
+ repositoryId: string;
63
+ resultSha: string;
64
+ workId: string;
65
+ runId: string;
66
+ operation: string;
67
+ }
68
+ export declare class LeaseLost extends HarnessError {
69
+ constructor(key: string);
70
+ }
71
+ export declare class ConnectorStore {
72
+ readonly file: string;
73
+ readonly identity: {
74
+ provider: ProviderId;
75
+ projectId: string;
76
+ };
77
+ readonly options: {
78
+ readOnly?: boolean;
79
+ onWrite?: (what: string) => void;
80
+ };
81
+ private readonly db;
82
+ /**
83
+ * `onWrite` — точка отказа для тестов: вызывается перед каждой записью с её
84
+ * названием и может бросить исключение (запись не выполняется).
85
+ */
86
+ constructor(file: string, identity: {
87
+ provider: ProviderId;
88
+ projectId: string;
89
+ }, options?: {
90
+ readOnly?: boolean;
91
+ onWrite?: (what: string) => void;
92
+ });
93
+ private checkIdentity;
94
+ close(): void;
95
+ /** Синхронная транзакция записи; внутри нет сети, процессов и `await`. */
96
+ private tx;
97
+ putSnapshot(record: Omit<SnapshotRecord, 'createdAt'>): {
98
+ record: SnapshotRecord;
99
+ created: boolean;
100
+ };
101
+ snapshot(revision: string): SnapshotRecord | undefined;
102
+ /** Последний снимок графа (по времени записи). */
103
+ latestSnapshot(graphId: string): SnapshotRecord | undefined;
104
+ /** Последний закреплённый снимок любого графа этого коннектора. */
105
+ latestSnapshotAny(): SnapshotRecord | undefined;
106
+ /**
107
+ * Взять владение операцией. Действующий lease другого владельца — отказ
108
+ * (`undefined` с текущим держателем); истёкший — перехват с новым токеном
109
+ * и увеличенным fence. Intent операции при этом не теряется: он в
110
+ * отдельной записи.
111
+ */
112
+ acquire(key: string, owner: string, ms: number): {
113
+ lease?: LeaseRecord;
114
+ holder?: LeaseRecord;
115
+ };
116
+ lease(key: string): LeaseRecord | undefined;
117
+ private assertLease;
118
+ /** Продлить владение; утраченное — `LeaseLost`. */
119
+ renew(key: string, token: string, ms: number): void;
120
+ release(key: string, token: string): void;
121
+ operation(key: string): OperationRecord | undefined;
122
+ operations(filter?: {
123
+ workId?: string;
124
+ taskId?: string;
125
+ }): OperationRecord[];
126
+ /** Записать intent до любого внешнего вызова; повтор существующего не меняет его. */
127
+ putIntent(record: Omit<OperationRecord, 'createdAt' | 'updatedAt' | 'status' | 'steps' | 'planned'>, token: string): OperationRecord;
128
+ /** Изменить операцию под владением `token` (fencing в той же транзакции). */
129
+ updateOp(key: string, token: string, mutate: (op: OperationRecord) => OperationRecord, what?: string): OperationRecord;
130
+ private writeOp;
131
+ putReceipt(receipt: ReceiptRecord, token: string): void;
132
+ receipts(orderId?: string): ReceiptRecord[];
133
+ history<T>(key: string): T | undefined;
134
+ histories<T>(prefix: string): {
135
+ key: string;
136
+ value: T;
137
+ }[];
138
+ /** Записать связь истории; `mutate` получает прежнее значение. */
139
+ putHistory<T>(key: string, mutate: (prior: T | undefined) => T): T;
140
+ }
@@ -0,0 +1,277 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { HarnessError } from "../../contracts/index.js";
6
+ /**
7
+ * Служебная запись коннектора — отдельная SQLite
8
+ * `<stateDir>/connectors/<provider>-<hash(projectId)>.sqlite`. Это не
9
+ * состояние исполнения (оно остаётся в `state.sqlite` harness): здесь только
10
+ * минимальные данные восстановления — снимки (manifest и digest), операции
11
+ * доставки (intent, шаги, receipt), владение доставкой (lease) и связи с
12
+ * историей backend.
13
+ *
14
+ * Каждая запись — отдельная транзакция `BEGIN IMMEDIATE` без сети и `await`
15
+ * внутри. Записи операции, начиная с владения, проверяют lease в той же
16
+ * транзакции: процесс, потерявший владение, ничего не записывает.
17
+ */
18
+ const SCHEMA_VERSION = '1';
19
+ export class LeaseLost extends HarnessError {
20
+ constructor(key) {
21
+ super('LEASE_STALE', 'Владение доставкой утрачено: ' + key.slice(0, 16));
22
+ }
23
+ }
24
+ const now = () => new Date().toISOString();
25
+ export class ConnectorStore {
26
+ file;
27
+ identity;
28
+ options;
29
+ db;
30
+ /**
31
+ * `onWrite` — точка отказа для тестов: вызывается перед каждой записью с её
32
+ * названием и может бросить исключение (запись не выполняется).
33
+ */
34
+ constructor(file, identity, options = {}) {
35
+ this.file = file;
36
+ this.identity = identity;
37
+ this.options = options;
38
+ if (options.readOnly) {
39
+ if (!existsSync(file))
40
+ throw new HarnessError('NOT_FOUND', 'Состояние коннектора не найдено');
41
+ this.db = new DatabaseSync(file, { readOnly: true });
42
+ this.db.exec('PRAGMA busy_timeout=20000;');
43
+ this.checkIdentity();
44
+ return;
45
+ }
46
+ mkdirSync(dirname(file), { recursive: true });
47
+ this.db = new DatabaseSync(file);
48
+ this.db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=20000; PRAGMA synchronous=FULL;');
49
+ this.tx(() => {
50
+ this.db.exec(`
51
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
52
+ CREATE TABLE IF NOT EXISTS snapshots (revision TEXT PRIMARY KEY, graph_id TEXT NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL);
53
+ CREATE TABLE IF NOT EXISTS operations (key TEXT PRIMARY KEY, work_id TEXT NOT NULL, run_id TEXT NOT NULL, task_id TEXT NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL, updated_at TEXT NOT NULL);
54
+ CREATE TABLE IF NOT EXISTS leases (key TEXT PRIMARY KEY, token TEXT NOT NULL, owner TEXT NOT NULL, expires_at INTEGER NOT NULL, fence INTEGER NOT NULL);
55
+ CREATE TABLE IF NOT EXISTS receipts (operation TEXT PRIMARY KEY, order_id TEXT NOT NULL, data TEXT NOT NULL);
56
+ CREATE TABLE IF NOT EXISTS history (key TEXT PRIMARY KEY, data TEXT NOT NULL, updated_at TEXT NOT NULL);`);
57
+ const ins = this.db.prepare('INSERT OR IGNORE INTO meta VALUES(?, ?)');
58
+ ins.run('schema', SCHEMA_VERSION);
59
+ ins.run('provider', identity.provider);
60
+ ins.run('projectId', identity.projectId);
61
+ });
62
+ this.checkIdentity();
63
+ }
64
+ checkIdentity() {
65
+ const get = (k) => this.db.prepare('SELECT value FROM meta WHERE key=?').get(k)?.value;
66
+ if (get('schema') !== SCHEMA_VERSION)
67
+ throw new HarnessError('STATE_VERSION_UNSUPPORTED', 'Состояние коннектора другой версии: ' + String(get('schema')));
68
+ if (get('provider') !== this.identity.provider || get('projectId') !== this.identity.projectId)
69
+ throw new HarnessError('INVALID_INPUT', 'Состояние коннектора принадлежит другому provider/проекту: ' + this.file);
70
+ }
71
+ close() {
72
+ this.db.close();
73
+ }
74
+ /** Синхронная транзакция записи; внутри нет сети, процессов и `await`. */
75
+ tx(fn, what) {
76
+ if (what)
77
+ this.options.onWrite?.(what);
78
+ this.db.exec('BEGIN IMMEDIATE');
79
+ try {
80
+ const out = fn();
81
+ this.db.exec('COMMIT');
82
+ return out;
83
+ }
84
+ catch (error) {
85
+ if (this.db.isTransaction)
86
+ this.db.exec('ROLLBACK');
87
+ throw error;
88
+ }
89
+ }
90
+ /* ---------------------------------------------------------------- */
91
+ /* Снимки */
92
+ /* ---------------------------------------------------------------- */
93
+ putSnapshot(record) {
94
+ return this.tx(() => {
95
+ const prior = this.snapshot(record.revision);
96
+ // Повтор той же редакции не меняет записанный снимок (и время его создания).
97
+ if (prior)
98
+ return { record: prior, created: false };
99
+ const full = { ...record, createdAt: now() };
100
+ this.db
101
+ .prepare('INSERT INTO snapshots VALUES(?, ?, ?, ?)')
102
+ .run(record.revision, record.graphId, JSON.stringify(full), full.createdAt);
103
+ return { record: full, created: true };
104
+ }, 'snapshot');
105
+ }
106
+ snapshot(revision) {
107
+ const row = this.db.prepare('SELECT data FROM snapshots WHERE revision=?').get(revision);
108
+ return row ? JSON.parse(row.data) : undefined;
109
+ }
110
+ /** Последний снимок графа (по времени записи). */
111
+ latestSnapshot(graphId) {
112
+ const row = this.db
113
+ .prepare('SELECT data FROM snapshots WHERE graph_id=? ORDER BY created_at DESC, rowid DESC LIMIT 1')
114
+ .get(graphId);
115
+ return row ? JSON.parse(row.data) : undefined;
116
+ }
117
+ /** Последний закреплённый снимок любого графа этого коннектора. */
118
+ latestSnapshotAny() {
119
+ const row = this.db
120
+ .prepare('SELECT data FROM snapshots ORDER BY created_at DESC, rowid DESC LIMIT 1')
121
+ .get();
122
+ return row ? JSON.parse(row.data) : undefined;
123
+ }
124
+ /* ---------------------------------------------------------------- */
125
+ /* Владение доставкой */
126
+ /* ---------------------------------------------------------------- */
127
+ /**
128
+ * Взять владение операцией. Действующий lease другого владельца — отказ
129
+ * (`undefined` с текущим держателем); истёкший — перехват с новым токеном
130
+ * и увеличенным fence. Intent операции при этом не теряется: он в
131
+ * отдельной записи.
132
+ */
133
+ acquire(key, owner, ms) {
134
+ return this.tx(() => {
135
+ const prior = this.lease(key);
136
+ const at = Date.now();
137
+ if (prior && prior.expiresAt > at)
138
+ return { holder: prior };
139
+ const lease = {
140
+ key,
141
+ token: randomUUID(),
142
+ owner,
143
+ expiresAt: at + ms,
144
+ fence: (prior?.fence ?? 0) + 1,
145
+ };
146
+ this.db
147
+ .prepare('INSERT OR REPLACE INTO leases VALUES(?, ?, ?, ?, ?)')
148
+ .run(key, lease.token, owner, lease.expiresAt, lease.fence);
149
+ return { lease };
150
+ }, 'lease');
151
+ }
152
+ lease(key) {
153
+ const row = this.db.prepare('SELECT * FROM leases WHERE key=?').get(key);
154
+ return row
155
+ ? {
156
+ key: row.key,
157
+ token: row.token,
158
+ owner: row.owner,
159
+ expiresAt: row.expires_at,
160
+ fence: row.fence,
161
+ }
162
+ : undefined;
163
+ }
164
+ assertLease(key, token) {
165
+ const lease = this.lease(key);
166
+ if (!lease || lease.token !== token || lease.expiresAt <= Date.now())
167
+ throw new LeaseLost(key);
168
+ }
169
+ /** Продлить владение; утраченное — `LeaseLost`. */
170
+ renew(key, token, ms) {
171
+ this.tx(() => {
172
+ this.assertLease(key, token);
173
+ this.db
174
+ .prepare('UPDATE leases SET expires_at=? WHERE key=? AND token=?')
175
+ .run(Date.now() + ms, key, token);
176
+ }, 'lease.renew');
177
+ }
178
+ release(key, token) {
179
+ try {
180
+ this.tx(() => {
181
+ this.db.prepare('UPDATE leases SET expires_at=0 WHERE key=? AND token=?').run(key, token);
182
+ });
183
+ }
184
+ catch {
185
+ /* Невозможность освободить — lease истечёт сам. */
186
+ }
187
+ }
188
+ /* ---------------------------------------------------------------- */
189
+ /* Операции доставки */
190
+ /* ---------------------------------------------------------------- */
191
+ operation(key) {
192
+ const row = this.db.prepare('SELECT data FROM operations WHERE key=?').get(key);
193
+ return row ? JSON.parse(row.data) : undefined;
194
+ }
195
+ operations(filter = {}) {
196
+ const rows = this.db
197
+ .prepare('SELECT data FROM operations WHERE (:w IS NULL OR work_id=:w) AND (:t IS NULL OR task_id=:t) ORDER BY rowid')
198
+ .all({ w: filter.workId ?? null, t: filter.taskId ?? null });
199
+ return rows.map((r) => JSON.parse(r.data));
200
+ }
201
+ /** Записать intent до любого внешнего вызова; повтор существующего не меняет его. */
202
+ putIntent(record, token) {
203
+ return this.tx(() => {
204
+ this.assertLease(record.key, token);
205
+ const prior = this.operation(record.key);
206
+ if (prior)
207
+ return prior;
208
+ const full = {
209
+ ...record,
210
+ status: 'intent',
211
+ steps: [],
212
+ planned: false,
213
+ createdAt: now(),
214
+ updatedAt: now(),
215
+ };
216
+ this.writeOp(full);
217
+ return full;
218
+ }, 'intent');
219
+ }
220
+ /** Изменить операцию под владением `token` (fencing в той же транзакции). */
221
+ updateOp(key, token, mutate, what = 'operation') {
222
+ return this.tx(() => {
223
+ this.assertLease(key, token);
224
+ const prior = this.operation(key);
225
+ if (!prior)
226
+ throw new HarnessError('NOT_FOUND', 'Операция доставки не найдена');
227
+ const next = { ...mutate(structuredClone(prior)), updatedAt: now() };
228
+ this.writeOp(next);
229
+ return next;
230
+ }, what);
231
+ }
232
+ writeOp(op) {
233
+ this.db
234
+ .prepare('INSERT OR REPLACE INTO operations VALUES(?, ?, ?, ?, ?, ?, ?)')
235
+ .run(op.key, op.workId, op.runId, op.taskId, op.status, JSON.stringify(op), op.updatedAt);
236
+ }
237
+ /* ---------------------------------------------------------------- */
238
+ /* Доверенные receipts */
239
+ /* ---------------------------------------------------------------- */
240
+ putReceipt(receipt, token) {
241
+ this.tx(() => {
242
+ this.assertLease(receipt.operation, token);
243
+ this.db
244
+ .prepare('INSERT OR REPLACE INTO receipts VALUES(?, ?, ?)')
245
+ .run(receipt.operation, receipt.orderId, JSON.stringify(receipt));
246
+ }, 'receipt');
247
+ }
248
+ receipts(orderId) {
249
+ const rows = this.db
250
+ .prepare('SELECT data FROM receipts WHERE (:o IS NULL OR order_id=:o) ORDER BY rowid')
251
+ .all({ o: orderId ?? null });
252
+ return rows.map((r) => JSON.parse(r.data));
253
+ }
254
+ /* ---------------------------------------------------------------- */
255
+ /* История */
256
+ /* ---------------------------------------------------------------- */
257
+ history(key) {
258
+ const row = this.db.prepare('SELECT data FROM history WHERE key=?').get(key);
259
+ return row ? JSON.parse(row.data) : undefined;
260
+ }
261
+ histories(prefix) {
262
+ const rows = this.db
263
+ .prepare('SELECT key, data FROM history WHERE substr(key, 1, length(:p)) = :p ORDER BY rowid')
264
+ .all({ p: prefix });
265
+ return rows.map((r) => ({ key: r.key, value: JSON.parse(r.data) }));
266
+ }
267
+ /** Записать связь истории; `mutate` получает прежнее значение. */
268
+ putHistory(key, mutate) {
269
+ return this.tx(() => {
270
+ const next = mutate(this.history(key));
271
+ this.db
272
+ .prepare('INSERT OR REPLACE INTO history VALUES(?, ?, ?)')
273
+ .run(key, JSON.stringify(next), now());
274
+ return next;
275
+ }, 'history');
276
+ }
277
+ }
@@ -0,0 +1,38 @@
1
+ import type { RunResult } from '../../contracts/index.ts';
2
+ import { type Redactor } from '../../environment/redaction.ts';
3
+ /**
4
+ * Краткий итог для внешнего хранилища задач. В нём: исход, проверяемые SHA,
5
+ * сводка gates, сопоставление критериев, источник снимка и receipt. Не
6
+ * попадают: полный runtime log, вызовы инструментов, локальные пути (они не
7
+ * являются ссылками, доступными с другого компьютера), lease/fencing токены
8
+ * и секреты (маскируются `redact`).
9
+ */
10
+ export interface CriterionOutcome {
11
+ criterionId: string;
12
+ title: string;
13
+ gate?: string;
14
+ testId?: string;
15
+ /** `closed` — закрыт этой доставкой; `already` — уже был отмечен; остальные — блокеры. */
16
+ outcome: 'closed' | 'already' | 'unproven' | 'manual';
17
+ reason?: string;
18
+ }
19
+ export declare const stripPaths: (text: string) => string;
20
+ export declare function summaryText(input: {
21
+ marker: string;
22
+ result: RunResult;
23
+ task: {
24
+ key: string;
25
+ identity: string;
26
+ };
27
+ snapshot: {
28
+ revision: string;
29
+ casRevision: number;
30
+ };
31
+ criteria: CriterionOutcome[];
32
+ completion: string;
33
+ historical?: boolean;
34
+ redact?: Redactor;
35
+ }): {
36
+ title: string;
37
+ body: string;
38
+ };