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
@@ -50,6 +50,26 @@ const entities = (text) => text.replace(/&(quot|apos|lt|gt|amp);/g, (_m, name) =
50
50
  * Изменённое так имя не совпадёт с названным критерием, и подтверждения
51
51
  * не будет: для секрета в имени теста это правильный исход.
52
52
  */
53
+ /**
54
+ * Значение атрибута XML: парсер работает без обработки сущностей (DOCTYPE
55
+ * запрещён, внешних сущностей нет), поэтому предопределённые сущности и
56
+ * числовые ссылки раскрываются здесь — один раз, без рекурсии. Иначе testcase
57
+ * `a["b"]` сохранялся как `a["b"]` и не совпадал с названным
58
+ * testId требования или критерия. Репортер JUnit Node.js экранирует `"`
59
+ * дважды (`"`); оставшийся после раскрытия `"` поэтому тоже
60
+ * читается как `"`.
61
+ */
62
+ export function xmlAttribute(value) {
63
+ const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
64
+ return value
65
+ .replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (all, ref) => {
66
+ if (!ref.startsWith('#'))
67
+ return named[ref] ?? all;
68
+ const code = ref.startsWith('#x') ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
69
+ return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : all;
70
+ })
71
+ .replace(/&quot;/g, '"');
72
+ }
53
73
  export function junitSummary(xml, redact = (value) => value) {
54
74
  if (xml.includes('<!DOCTYPE') || XMLValidator.validate(xml) !== true)
55
75
  throw new Error('Некорректный JUnit XML');
@@ -75,7 +95,10 @@ export function junitSummary(xml, redact = (value) => value) {
75
95
  summary.tests++;
76
96
  if (value && typeof value === 'object') {
77
97
  const named = value;
78
- const raw = [named['@_classname'], named['@_name']].filter(Boolean).join('.');
98
+ const raw = [named['@_classname'], named['@_name']]
99
+ .filter(Boolean)
100
+ .map((v) => xmlAttribute(String(v)))
101
+ .join('.');
79
102
  const redacted = redact(raw);
80
103
  // Длинное имя заменяется хешем: обрезка дала бы коллизии и ложные
81
104
  // совпадения, а без замены receipt не перенёс бы манифест.
@@ -228,7 +251,14 @@ export async function prepareReportPath(root, path, escape) {
228
251
  export async function runCheck(
229
252
  // Нужна только политика изоляции: проверки ревьюера исполняются вне сервера.
230
253
  h, options) {
231
- let argv = options.argv, env = options.env, started;
254
+ let launched = false;
255
+ const confirm = () => {
256
+ if (launched)
257
+ return;
258
+ launched = true;
259
+ options.onStart?.();
260
+ };
261
+ let argv = options.argv, env = options.env, started, result, attempts = 1;
232
262
  const scratch = await realpath(await mkdtemp(join(tmpdir(), 'dc-gate-')));
233
263
  // Метка старта — вне TMPDIR команды: проверка, чистящая свой временный
234
264
  // каталог, не должна стирать признак того, что она уже начиналась.
@@ -252,39 +282,73 @@ h, options) {
252
282
  argv = wrapped.argv;
253
283
  env = { ...env, ...wrapped.env };
254
284
  }
255
- const execute = () => command(argv, options.cwd, {
256
- signal: options.signal,
257
- timeoutMs: options.timeoutMs,
258
- env,
259
- redact: options.redact,
260
- // Отсоединившийся потомок проверки не переживает её: и при успехе,
261
- // и по таймауту, отмене или падению.
262
- // Каталоги проверки принадлежат только ей: процесс, запущенный в них
263
- // во время проверки, — её потомок, даже без метки и родства.
264
- contain: { dirs: [...options.write, scratch, marks] },
265
- });
266
- let result = await execute(), attempts = 1;
267
- // sandbox-runtime ищет shell через `which` с таймаутом в секунду и под
268
- // нагрузкой отказывает до запуска команды. Повтор допустим, только если
269
- // команда точно не начиналась: метки старта нет.
270
- for (; attempts < 3 && sandboxNotStarted(result, started); attempts++) {
271
- await new Promise((r) => setTimeout(r, 500 * attempts));
285
+ const execute = () => {
286
+ // Уже отменённый вызов не запускается: слушатель отмены на отменённом
287
+ // сигнале не срабатывает, и команда шла бы до таймаута.
288
+ if (options.signal.aborted)
289
+ throw Object.assign(new Error('Проверка отменена до запуска'), { name: 'AbortError' });
290
+ return command(argv, options.cwd, {
291
+ signal: options.signal,
292
+ ...(started === undefined ? { onSpawn: confirm } : {}),
293
+ timeoutMs: options.timeoutMs,
294
+ env,
295
+ redact: options.redact,
296
+ // Отсоединившийся потомок проверки не переживает её: и при успехе,
297
+ // и по таймауту, отмене или падению.
298
+ // Каталоги проверки принадлежат только ей: процесс, запущенный в них
299
+ // во время проверки, — её потомок, даже без метки и родства.
300
+ contain: { dirs: [...options.write, scratch, marks] },
301
+ });
302
+ };
303
+ try {
272
304
  result = await execute();
305
+ // sandbox-runtime ищет shell через `which` с таймаутом в секунду и под
306
+ // нагрузкой отказывает до запуска команды. Повтор допустим, только если
307
+ // команда точно не начиналась: метки старта нет.
308
+ for (; attempts < 3 && sandboxNotStarted(result, started); attempts++) {
309
+ await new Promise((r) => setTimeout(r, 500 * attempts));
310
+ result = await execute();
311
+ }
312
+ }
313
+ finally {
314
+ // Факт старта читается до удаления метки — и при исключении после него.
315
+ if (started !== undefined && existsSync(started))
316
+ confirm();
273
317
  }
274
- return { ...result, attempts };
318
+ // Без песочницы завершившийся процесс точно порождался.
319
+ if (started === undefined)
320
+ confirm();
321
+ return { ...result, attempts, started: launched };
275
322
  }
276
323
  finally {
277
324
  // Каждый каталог убирается независимо: отказ уборки временного каталога
278
325
  // проверки (она могла снять с него права) не должен оставлять и метки.
279
326
  const removed = await Promise.allSettled([
280
- rm(scratch, { recursive: true, force: true }),
281
- rm(marks, { recursive: true, force: true }),
327
+ checkCleanup.remove(scratch),
328
+ checkCleanup.remove(marks),
282
329
  ]);
283
330
  const failed = removed.find((r) => r.status === 'rejected');
284
- if (failed)
285
- throw failed.reason;
331
+ if (failed) {
332
+ // Отказ уборки после завершения не стирает исход проверки: код и
333
+ // вывод доступны вызывающему через ошибку (`completedCheck`).
334
+ const error = failed.reason instanceof Error ? failed.reason : new Error(String(failed.reason));
335
+ throw result
336
+ ? Object.assign(error, { [checkResult]: { ...result, attempts, started: launched } })
337
+ : error;
338
+ }
286
339
  }
287
340
  }
341
+ const checkResult = Symbol('runCheck.result');
342
+ /** Исход проверки, завершившейся до исключения (например, отказа уборки), если он есть. */
343
+ export function completedCheck(error) {
344
+ return error && typeof error === 'object' && checkResult in error
345
+ ? error[checkResult]
346
+ : undefined;
347
+ }
348
+ /** Уборка каталогов проверки; тест подменяет её, чтобы проверить отказ после исхода. */
349
+ export const checkCleanup = {
350
+ remove: (dir) => rm(dir, { recursive: true, force: true }),
351
+ };
288
352
  /** Проверка механизма песочницы; тест подменяет её, чтобы проверить отказ. */
289
353
  export const isolationBackend = { check: isolationSupport };
290
354
  /**
@@ -305,6 +369,16 @@ export function sandboxBlockedHosts(output, allowed) {
305
369
  return [...hosts].filter((host) => host !== '127.0.0.1' &&
306
370
  !allowed.some((d) => host === d.toLowerCase() || host.endsWith('.' + d.toLowerCase())));
307
371
  }
372
+ /**
373
+ * Окружение свежести test gate: те же переменные у gate движка и у gate,
374
+ * запущенного проверкой reviewer, — иначе probe мог бы прочитать cache там,
375
+ * где gate исполняет тесты заново.
376
+ */
377
+ export function freshnessEnv(kind, env) {
378
+ return kind === 'test'
379
+ ? { TURBO_FORCE: 'true', GOFLAGS: [env.GOFLAGS, '-count=1'].filter(Boolean).join(' ') }
380
+ : {};
381
+ }
308
382
  /**
309
383
  * Gate попытки. `test` принимается только по свежему (прежний отчёт удалён до
310
384
  * запуска) непустому JUnit без failures, errors и skips на этом SHA; exit 0
@@ -356,14 +430,22 @@ export async function runGate(run) {
356
430
  ...(corpusPath ? { DEVCONTOUR_CORPUS: corpusPath } : {}),
357
431
  }
358
432
  : {};
433
+ // Test gate исполняет тесты заново, через какую бы обёртку (package
434
+ // script, task runner) ни шла команда: Turbo не читает cache (`TURBO_FORCE`),
435
+ // Go не берёт результат из кэша тестов (`-count=1` в GOFLAGS, последним).
436
+ // Восстановленный из cache зелёный отчёт иначе выглядел бы свежим.
437
+ const freshness = freshnessEnv(gate.kind, run.execution.env);
359
438
  let kind = 'gate';
360
- let passed = false, summary = '', log = '', exitCode = -1, manifest;
439
+ // Заголовок лога — фактические каталог и argv запуска: digest лога входит в
440
+ // evidence, и запись называет, где и чем проверка исполнялась.
441
+ let passed = false, summary = '', log = `# harness gate ${gate.id}: cwd=${relative(root, gateCwd) || '.'} argv=${JSON.stringify(gate.command.map(redact))}${gate.kind === 'test' ? ` fresh=${redact(JSON.stringify(freshness))}` : ''}\n`, exitCode = -1, manifest;
361
442
  try {
362
443
  const result = await runCheck({ config: run.policy }, {
363
444
  argv: gate.command,
364
445
  cwd: gateCwd,
365
446
  env: {
366
447
  ...run.execution.env,
448
+ ...freshness,
367
449
  ...(reportPath
368
450
  ? { DEVCONTOUR_REPORT_PATH: reportPath, HARNESS_REPORT_PATH: reportPath }
369
451
  : {}),
@@ -379,7 +461,7 @@ export async function runGate(run) {
379
461
  settingsDir: join(artifactDir, gate.id),
380
462
  });
381
463
  exitCode = result.code;
382
- log = result.stdout + '\n' + result.stderr;
464
+ log += result.stdout + '\n' + result.stderr;
383
465
  if (result.timedOut || signal.aborted)
384
466
  throw new Error('Проверка прервана или превысила timeout');
385
467
  if (result.code !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcontour-harness",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "Headless execution harness: work graph, isolated attempts, gates, independent reviews and accepted results.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,26 @@
32
32
  "./testing": {
33
33
  "types": "./lib/testing/index.d.ts",
34
34
  "default": "./lib/testing/index.js"
35
+ },
36
+ "./toolchains": {
37
+ "types": "./lib/toolchains/index.d.ts",
38
+ "default": "./lib/toolchains/index.js"
39
+ },
40
+ "./integrations/connector": {
41
+ "types": "./lib/integrations/connector/index.d.ts",
42
+ "default": "./lib/integrations/connector/index.js"
43
+ },
44
+ "./integrations/relay-v1": {
45
+ "types": "./lib/integrations/relay-v1/index.d.ts",
46
+ "default": "./lib/integrations/relay-v1/index.js"
47
+ },
48
+ "./integrations/relay-v2": {
49
+ "types": "./lib/integrations/relay-v2/index.d.ts",
50
+ "default": "./lib/integrations/relay-v2/index.js"
51
+ },
52
+ "./integrations/memory": {
53
+ "types": "./lib/integrations/memory/index.d.ts",
54
+ "default": "./lib/integrations/memory/index.js"
35
55
  }
36
56
  },
37
57
  "files": [
@@ -58,6 +78,7 @@
58
78
  "schemas:check": "node --import tsx scripts/build-schemas.ts --check",
59
79
  "lint": "eslint .",
60
80
  "test": "node scripts/test.mjs",
81
+ "test:integration": "node --import tsx --test --test-concurrency=1 --test-timeout=1800000 tests/integration/*.test.ts",
61
82
  "test:package": "npm run build && node scripts/smoke-package.mjs",
62
83
  "check": "npm run lint && npm run typecheck && npm run schemas:check && npm run build && npm test && node scripts/smoke-package.mjs",
63
84
  "format": "prettier --write .",
@@ -0,0 +1,21 @@
1
+ {
2
+ "id": "go-gotestsum",
3
+ "version": "0.1.0",
4
+ "capabilities": ["go", "unit-tests"],
5
+ "protectedPaths": ["go.mod", "go.sum"],
6
+ "toolchain": {
7
+ "tools": { "gotestsum": "1.12.3" }
8
+ },
9
+ "prepare": [{ "id": "install", "command": ["go", "mod", "download"], "timeoutMs": 600000 }],
10
+ "gates": [
11
+ { "id": "vet", "kind": "check", "command": ["go", "vet", "./..."], "timeoutMs": 240000 },
12
+ {
13
+ "id": "acceptance",
14
+ "kind": "test",
15
+ "command": ["gotestsum", "--junitfile", ".reports/junit.xml", "--", "-count=1", "./..."],
16
+ "timeoutMs": 240000,
17
+ "dependsOn": ["vet"],
18
+ "report": { "type": "junit", "path": ".reports/junit.xml" }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "npm-ci",
3
+ "version": "0.1.0",
4
+ "capabilities": ["node", "npm", "unit-tests"],
5
+ "protectedPaths": ["package.json", "package-lock.json"],
6
+ "toolchain": {
7
+ "tools": { "npm": "11.4.2" },
8
+ "packageManagers": { ".": "npm" }
9
+ },
10
+ "prepare": [{ "id": "install", "command": ["npm", "ci"], "timeoutMs": 600000 }],
11
+ "gates": [
12
+ { "id": "check", "kind": "check", "command": ["npm", "run", "check"], "timeoutMs": 240000 },
13
+ {
14
+ "id": "unit-tests",
15
+ "kind": "test",
16
+ "command": ["npm", "run", "test"],
17
+ "timeoutMs": 240000,
18
+ "dependsOn": ["check"],
19
+ "report": { "type": "junit", "path": ".reports/junit.xml" }
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "pnpm-frozen",
3
+ "version": "0.1.0",
4
+ "capabilities": ["node", "pnpm", "unit-tests"],
5
+ "protectedPaths": ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"],
6
+ "toolchain": {
7
+ "tools": { "pnpm": "10.12.3" },
8
+ "packageManagers": { ".": "pnpm" }
9
+ },
10
+ "prepare": [
11
+ { "id": "install", "command": ["pnpm", "install", "--frozen-lockfile"], "timeoutMs": 600000 }
12
+ ],
13
+ "gates": [
14
+ { "id": "check", "kind": "check", "command": ["pnpm", "run", "check"], "timeoutMs": 240000 },
15
+ {
16
+ "id": "unit-tests",
17
+ "kind": "test",
18
+ "command": ["pnpm", "run", "test"],
19
+ "timeoutMs": 240000,
20
+ "dependsOn": ["check"],
21
+ "report": { "type": "junit", "path": ".reports/junit.xml" }
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "python-uv",
3
+ "version": "0.1.0",
4
+ "capabilities": ["python", "uv", "pytest"],
5
+ "protectedPaths": ["pyproject.toml", "uv.lock", ".python-version"],
6
+ "toolchain": {
7
+ "tools": { "uv": "0.11.28" }
8
+ },
9
+ "prepare": [{ "id": "install", "command": ["uv", "sync", "--locked"], "timeoutMs": 600000 }],
10
+ "gates": [
11
+ {
12
+ "id": "lint",
13
+ "kind": "check",
14
+ "command": ["uv", "run", "--no-sync", "ruff", "check", "."],
15
+ "timeoutMs": 240000
16
+ },
17
+ {
18
+ "id": "acceptance",
19
+ "kind": "test",
20
+ "command": ["uv", "run", "--no-sync", "pytest", "--junitxml=.reports/junit.xml"],
21
+ "timeoutMs": 240000,
22
+ "dependsOn": ["lint"],
23
+ "report": { "type": "junit", "path": ".reports/junit.xml" }
24
+ }
25
+ ]
26
+ }