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,4 @@
1
+ node_modules/
2
+ .reports/
3
+ .turbo/
4
+ .venv/
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "fx-label-turbo",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "packageManager": "pnpm@10.12.3",
6
+ "scripts": {
7
+ "check": "turbo run check",
8
+ "test": "turbo run test --force"
9
+ },
10
+ "devDependencies": {
11
+ "turbo": "2.11.10"
12
+ }
13
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Локальный бинарник workspace: синтаксическая проверка перечисленных файлов.
3
+ import { execFileSync } from 'node:child_process';
4
+ for (const file of process.argv.slice(2)) execFileSync(process.execPath, ['--check', file]);
5
+ console.log('fx-check: ok ' + process.argv.slice(2).join(' '));
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@fx/checker",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "bin": {
7
+ "fx-check": "./check.mjs"
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@fx/label",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "check": "fx-check src/label.js test/label.test.js",
8
+ "test": "mkdir -p .reports && node --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=.reports/junit.xml test/*.test.js"
9
+ },
10
+ "devDependencies": {
11
+ "@fx/checker": "workspace:*",
12
+ "picocolors": "1.1.1"
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ // Метка позиции заказа.
2
+ export function label(n) {
3
+ return `item-${n}`;
4
+ }
@@ -0,0 +1,13 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { label } from '../src/label.js';
4
+
5
+ test('label pads one digit to three', () => {
6
+ assert.equal(label(7), 'item-007');
7
+ });
8
+ test('label pads two digits to three', () => {
9
+ assert.equal(label(42), 'item-042');
10
+ });
11
+ test('label keeps long numbers', () => {
12
+ assert.equal(label(1234), 'item-1234');
13
+ });
@@ -0,0 +1,94 @@
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ devDependencies:
11
+ turbo:
12
+ specifier: 2.11.10
13
+ version: 2.11.10
14
+
15
+ packages/checker: {}
16
+
17
+ packages/label:
18
+ devDependencies:
19
+ '@fx/checker':
20
+ specifier: workspace:*
21
+ version: link:../checker
22
+ picocolors:
23
+ specifier: 1.1.1
24
+ version: 1.1.1
25
+
26
+ packages:
27
+
28
+ '@turbo/darwin-64@2.11.10':
29
+ resolution: {integrity: sha512-By4idRap5+nLnMuRzLB4qkOPRlvNdyY1ne4VJYDyUoUB11H94k7uif9mbe3JT8JSNXs1D68uMcBj6hJ/jMQRvA==}
30
+ cpu: [x64]
31
+ os: [darwin]
32
+
33
+ '@turbo/darwin-arm64@2.11.10':
34
+ resolution: {integrity: sha512-pyEYlVcuexOPa92p0Wlwl9YT1Is5D60lxN3ItWm3g30YLvFaUq86TF4BBWg5ZAc0uMTnrOzCvuCsOBTdzUWnRQ==}
35
+ cpu: [arm64]
36
+ os: [darwin]
37
+
38
+ '@turbo/linux-64@2.11.10':
39
+ resolution: {integrity: sha512-1tYeR/IDZ5HEdsb0jNq2L2xam5nMQ+g80tlV2BZmv4OFw+pMcUM0mMmUcfc5wXPPNHVNPuDMjh9Y8G8HgXPLZA==}
40
+ cpu: [x64]
41
+ os: [android, linux]
42
+
43
+ '@turbo/linux-arm64@2.11.10':
44
+ resolution: {integrity: sha512-HuekQxvVGhUd46g1Im/DKdOtglHr4/gguZAvzEpPUgtxNkztSa8lWhQEGQ+EmPQ0n7w19qjThFU+OfEEd0uvlw==}
45
+ cpu: [arm64]
46
+ os: [android, linux]
47
+
48
+ '@turbo/windows-64@2.11.10':
49
+ resolution: {integrity: sha512-UJOP3BkbreY3/JlMgJR6ZiimxYIBn+c+B4tnBIxD0E9c3Estmk+XoVNVBNBottZLNRXe3/ScgOU4+/k8tIWysg==}
50
+ cpu: [x64]
51
+ os: [win32]
52
+
53
+ '@turbo/windows-arm64@2.11.10':
54
+ resolution: {integrity: sha512-xOkEjF3ws/b/YNlc+bvJHMYBn+vL+QbEISZFVi0qS+ImmhA1X3/gmNmBrjjIG3dg6aeN+n6gQitzSkXQ286qJQ==}
55
+ cpu: [arm64]
56
+ os: [win32]
57
+
58
+ picocolors@1.1.1:
59
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
60
+
61
+ turbo@2.11.10:
62
+ resolution: {integrity: sha512-3GJYwig8ygEI89oaOZdduJX8/aw6O9eRPYHoIt6T3FZl9hkKjiyIspufJoVgqXihPoJu8M5CSV0DlZvzfohBCw==}
63
+ hasBin: true
64
+
65
+ snapshots:
66
+
67
+ '@turbo/darwin-64@2.11.10':
68
+ optional: true
69
+
70
+ '@turbo/darwin-arm64@2.11.10':
71
+ optional: true
72
+
73
+ '@turbo/linux-64@2.11.10':
74
+ optional: true
75
+
76
+ '@turbo/linux-arm64@2.11.10':
77
+ optional: true
78
+
79
+ '@turbo/windows-64@2.11.10':
80
+ optional: true
81
+
82
+ '@turbo/windows-arm64@2.11.10':
83
+ optional: true
84
+
85
+ picocolors@1.1.1: {}
86
+
87
+ turbo@2.11.10:
88
+ optionalDependencies:
89
+ '@turbo/darwin-64': 2.11.10
90
+ '@turbo/darwin-arm64': 2.11.10
91
+ '@turbo/linux-64': 2.11.10
92
+ '@turbo/linux-arm64': 2.11.10
93
+ '@turbo/windows-64': 2.11.10
94
+ '@turbo/windows-arm64': 2.11.10
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - 'packages/*'
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://turborepo.com/schema.json",
3
+ "tasks": {
4
+ "check": { "inputs": ["src/**", "test/**", "package.json"] },
5
+ "test": { "dependsOn": ["check"], "outputs": [".reports/**"] }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "title": "Pad item labels to three digits",
3
+ "goal": "Change label(n) in packages/label/src/label.js so the number is zero-padded to three digits (item-007), keeping longer numbers unchanged.",
4
+ "acceptance": [
5
+ "label(7) is item-007 and label(42) is item-042",
6
+ "label(1234) is item-1234",
7
+ "All gates pass with fresh JUnit reports"
8
+ ],
9
+ "writeScope": ["packages/label/src"],
10
+ "gates": ["check", "unit-tests"],
11
+ "requirements": [
12
+ {
13
+ "id": "REQ-LABEL-PAD",
14
+ "gate": "unit-tests",
15
+ "testId": "test.label pads one digit to three",
16
+ "scenario": "test.label pads one digit to three runs and passes on the accepted SHA"
17
+ },
18
+ {
19
+ "id": "REQ-LABEL-LONG",
20
+ "gate": "unit-tests",
21
+ "testId": "test.label keeps long numbers",
22
+ "scenario": "test.label keeps long numbers runs and passes on the accepted SHA"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,16 @@
1
+ # Turbo Starter (pnpm + Turborepo, Docker lifecycle)
2
+
3
+ Рецепт для копии [oim-dev/turbo-starter](https://github.com/oim-dev/turbo-starter)
4
+ на commit `78f1e76b80f76ae9d4289e0b0994df8a6f2b89cc`. Изменение контракта backend
5
+ `AccessTokenDto.issuedAt` проходит путь до OpenAPI, затем до обоих SDK и до потребителя
6
+ frontend. Работу проверяют настоящие команды starter и JUnit-проверки примера.
7
+
8
+ - `policy.json` и `graph.json`: основной сценарий (gates `backend-types`,
9
+ `sdk-generation`, `sdk-build`, `web-types`, `acceptance`).
10
+ - `policy.lifecycle.json`: lifecycle `compose-bridge` (PostgreSQL на попытку и фазу).
11
+ - `harness/`: копируется в `.harness/` проекта. Внутри мост, compose-стек приёмки,
12
+ шаблоны env и проверки `node --test`.
13
+ - `changes/`: подготовленные изменения для fixture writer, верное и несовместимое.
14
+
15
+ Подробности описаны в [docs/turbo-starter.md](../../docs/turbo-starter.md): как повторить,
16
+ схема генерации, мост, порядок принятия, controller mode и ограничения upstream.
@@ -0,0 +1,12 @@
1
+ diff --git a/apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts b/apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts
2
+ new file mode 100644
3
+ index 0000000..2df3a5e
4
+ --- /dev/null
5
+ +++ b/apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts
6
+ @@ -0,0 +1,6 @@
7
+ +import type { AccessTokenDto } from '@oim/client-rest-api-sdk/data-contracts'
8
+ +
9
+ +/**
10
+ + * Момент выдачи токена из ответа входа (ISO date-time).
11
+ + */
12
+ +export type AccessTokenIssuedAt = AccessTokenDto['issuedAt']
@@ -0,0 +1,130 @@
1
+ diff --git a/apps/nest-backend/openapi/admin.openapi.json b/apps/nest-backend/openapi/admin.openapi.json
2
+ index 7d98313..a11c859 100644
3
+ --- a/apps/nest-backend/openapi/admin.openapi.json
4
+ +++ b/apps/nest-backend/openapi/admin.openapi.json
5
+ @@ -6958,13 +6958,19 @@
6
+ "type": "string",
7
+ "format": "date-time",
8
+ "description": "Фиксированный срок окончания сессии, совпадающий с exp JWT. Сессия может быть отозвана раньше при выходе, смене credentials или отзыве доступа."
9
+ + },
10
+ + "issuedAt": {
11
+ + "type": "string",
12
+ + "format": "date-time",
13
+ + "description": "Момент выдачи JWT, совпадающий с iat."
14
+ }
15
+ },
16
+ "required": [
17
+ "accessToken",
18
+ "tokenType",
19
+ "expiresIn",
20
+ - "sessionExpiresAt"
21
+ + "sessionExpiresAt",
22
+ + "issuedAt"
23
+ ]
24
+ },
25
+ "AdminUserDto": {
26
+ diff --git a/apps/nest-backend/openapi/client.openapi.json b/apps/nest-backend/openapi/client.openapi.json
27
+ index 89003d9..f2e6dee 100644
28
+ --- a/apps/nest-backend/openapi/client.openapi.json
29
+ +++ b/apps/nest-backend/openapi/client.openapi.json
30
+ @@ -3179,13 +3179,19 @@
31
+ "type": "string",
32
+ "format": "date-time",
33
+ "description": "Фиксированный срок окончания сессии, совпадающий с exp JWT. Сессия может быть отозвана раньше при выходе, смене credentials или отзыве доступа."
34
+ + },
35
+ + "issuedAt": {
36
+ + "type": "string",
37
+ + "format": "date-time",
38
+ + "description": "Момент выдачи JWT, совпадающий с iat."
39
+ }
40
+ },
41
+ "required": [
42
+ "accessToken",
43
+ "tokenType",
44
+ "expiresIn",
45
+ - "sessionExpiresAt"
46
+ + "sessionExpiresAt",
47
+ + "issuedAt"
48
+ ]
49
+ },
50
+ "UpdateClientUserProfileDto": {
51
+ diff --git a/apps/nest-backend/src/infrastructure/auth/auth.dto.ts b/apps/nest-backend/src/infrastructure/auth/auth.dto.ts
52
+ index c69eb4c..4d1ea19 100644
53
+ --- a/apps/nest-backend/src/infrastructure/auth/auth.dto.ts
54
+ +++ b/apps/nest-backend/src/infrastructure/auth/auth.dto.ts
55
+ @@ -48,4 +48,11 @@ export class AccessTokenDto {
56
+ 'Фиксированный срок окончания сессии, совпадающий с exp JWT. Сессия может быть отозвана раньше при выходе, смене credentials или отзыве доступа.',
57
+ })
58
+ sessionExpiresAt!: Date;
59
+ +
60
+ + @ApiProperty({
61
+ + type: String,
62
+ + format: 'date-time',
63
+ + description: 'Момент выдачи JWT, совпадающий с iat.',
64
+ + })
65
+ + issuedAt!: Date;
66
+ }
67
+ diff --git a/apps/nest-backend/src/infrastructure/auth/session.service.ts b/apps/nest-backend/src/infrastructure/auth/session.service.ts
68
+ index 32c52f1..f62e1b5 100644
69
+ --- a/apps/nest-backend/src/infrastructure/auth/session.service.ts
70
+ +++ b/apps/nest-backend/src/infrastructure/auth/session.service.ts
71
+ @@ -14,6 +14,7 @@ export interface IssuedAccessToken {
72
+ tokenType: 'Bearer';
73
+ expiresIn: number;
74
+ sessionExpiresAt: Date;
75
+ + issuedAt: Date;
76
+ }
77
+
78
+ @Injectable()
79
+ @@ -104,6 +105,7 @@ export class SessionService {
80
+ tokenType: 'Bearer',
81
+ expiresIn,
82
+ sessionExpiresAt: session.expiresAt,
83
+ + issuedAt: new Date(issuedAt * 1000),
84
+ };
85
+ }
86
+ }
87
+ diff --git a/apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts b/apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts
88
+ new file mode 100644
89
+ index 0000000..2df3a5e
90
+ --- /dev/null
91
+ +++ b/apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts
92
+ @@ -0,0 +1,6 @@
93
+ +import type { AccessTokenDto } from '@oim/client-rest-api-sdk/data-contracts'
94
+ +
95
+ +/**
96
+ + * Момент выдачи токена из ответа входа (ISO date-time).
97
+ + */
98
+ +export type AccessTokenIssuedAt = AccessTokenDto['issuedAt']
99
+ diff --git a/packages/admin-rest-api-sdk/src/generated/data-contracts.ts b/packages/admin-rest-api-sdk/src/generated/data-contracts.ts
100
+ index 782cd48..29941ec 100644
101
+ --- a/packages/admin-rest-api-sdk/src/generated/data-contracts.ts
102
+ +++ b/packages/admin-rest-api-sdk/src/generated/data-contracts.ts
103
+ @@ -520,6 +520,11 @@ export interface AccessTokenDto {
104
+ * @format date-time
105
+ */
106
+ sessionExpiresAt: string;
107
+ + /**
108
+ + * Момент выдачи JWT, совпадающий с iat.
109
+ + * @format date-time
110
+ + */
111
+ + issuedAt: string;
112
+ }
113
+
114
+ export interface AdminUserDto {
115
+ diff --git a/packages/client-rest-api-sdk/src/generated/data-contracts.ts b/packages/client-rest-api-sdk/src/generated/data-contracts.ts
116
+ index 888aba5..b2f9e59 100644
117
+ --- a/packages/client-rest-api-sdk/src/generated/data-contracts.ts
118
+ +++ b/packages/client-rest-api-sdk/src/generated/data-contracts.ts
119
+ @@ -258,6 +258,11 @@ export interface AccessTokenDto {
120
+ * @format date-time
121
+ */
122
+ sessionExpiresAt: string;
123
+ + /**
124
+ + * Момент выдачи JWT, совпадающий с iat.
125
+ + * @format date-time
126
+ + */
127
+ + issuedAt: string;
128
+ }
129
+
130
+ export interface UpdateClientUserProfileDto {
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Довести очистку попытки в новом процессе по сохранённому состоянию harness:
4
+ * повторить teardown lifecycle (compose-bridge) по receipt окружений и убрать
5
+ * рабочие деревья попытки. Нужен, если процесс harness был прерван при
6
+ * активной среде или teardown не удался (receipt `cleanup-failed`).
7
+ *
8
+ * node node_modules/devcontour-harness/examples/turbo-starter/cleanup.mjs --state <dir> --run <runId>
9
+ *
10
+ * Политика и baseDir берутся из `<state>/binding.json`, который пишет CLI.
11
+ * Секреты политики (`environment.secrets`) должны быть заданы в окружении так
12
+ * же, как при запуске. Печатает JSON { ok, data | error }; код 0 — очистка
13
+ * выполнена или не нужна. Только публичный API пакета, без сборщиков.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { parseArgs } from 'node:util';
18
+ import { Harness } from 'devcontour-harness';
19
+ import { executionPolicySchema } from 'devcontour-harness/contracts';
20
+
21
+ const { values } = parseArgs({ options: { state: { type: 'string' }, run: { type: 'string' } } });
22
+ if (!values.state || !values.run) {
23
+ console.error('usage: cleanup.mjs --state <dir> --run <runId>');
24
+ process.exit(2);
25
+ }
26
+ const stateDir = resolve(values.state);
27
+ const binding = JSON.parse(readFileSync(join(stateDir, 'binding.json'), 'utf8'));
28
+ const h = Harness.open({
29
+ stateDir,
30
+ policy: executionPolicySchema.parse(binding.policy),
31
+ baseDir: binding.baseDir,
32
+ });
33
+ try {
34
+ const data = await h.cleanup(values.run);
35
+ const attempt = h.engine.read((v) => v.attempt(values.run));
36
+ console.log(
37
+ JSON.stringify({
38
+ ok: true,
39
+ pid: process.pid,
40
+ data,
41
+ attempt: { status: attempt?.status, error: attempt?.error },
42
+ }),
43
+ );
44
+ } catch (error) {
45
+ console.log(
46
+ JSON.stringify({
47
+ ok: false,
48
+ pid: process.pid,
49
+ error: {
50
+ code: error?.code,
51
+ message: error instanceof Error ? error.message : String(error),
52
+ },
53
+ }),
54
+ );
55
+ process.exitCode = 1;
56
+ } finally {
57
+ h.close();
58
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "apiVersion": "devcontour-harness/v1alpha1",
3
+ "id": "turbo-starter-issued-at",
4
+ "source": { "connector": "files", "ref": "graph.json", "revision": "1" },
5
+ "orders": [
6
+ {
7
+ "apiVersion": "devcontour-harness/v1alpha1",
8
+ "id": "issued-at",
9
+ "source": { "connector": "files", "ref": "issued-at", "revision": "1" },
10
+ "title": "Expose AccessTokenDto.issuedAt end to end",
11
+ "goal": "Add a required date-time field issuedAt (the JWT iat) to the backend AccessTokenDto and the session service, regenerate OpenAPI and both REST SDKs with `pnpm rest-api-sdk:generate`, and add a typed web consumer type AccessTokenIssuedAt = AccessTokenDto['issuedAt'] in apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts.",
12
+ "acceptance": [
13
+ "client and admin OpenAPI AccessTokenDto declare issuedAt as a required date-time string",
14
+ "client and admin SDK AccessTokenDto declare issuedAt: string",
15
+ "committed OpenAPI/SDK outputs equal the pinned generator output (sdk-generation gate)",
16
+ "backend and web type checks pass",
17
+ "the web consumer type reads AccessTokenDto['issuedAt'] from the client SDK"
18
+ ],
19
+ "role": "developer",
20
+ "repositoryId": "starter",
21
+ "writeScope": [
22
+ "apps/nest-backend/src/infrastructure/auth",
23
+ "apps/nest-backend/openapi",
24
+ "packages/client-rest-api-sdk/src/generated",
25
+ "packages/admin-rest-api-sdk/src/generated",
26
+ "apps/next-web-app/src/infra/client-api/types"
27
+ ],
28
+ "gates": ["backend-types", "sdk-generation", "sdk-build", "web-types", "acceptance"],
29
+ "context": ["controller-mode"]
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,97 @@
1
+ // Gate `db-isolation` (S03): две попытки в своих worktree и своих Docker-проектах.
2
+ // Запуск через compose-bridge exec (env-файл окружения фазы даёт POSTGRES_* и HX_PROJECT):
3
+ // node .harness/compose-bridge.mjs exec -- node --test --test-reporter=junit \
4
+ // --test-reporter-destination=.artifacts/junit/db-isolation.xml .harness/acceptance/db-isolation.test.mjs
5
+ // Барьер сценария — каталог S03_BARRIER (явные файлы-события, без случайных задержек).
6
+ import { test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ const env = process.env;
13
+ const project = env.HX_PROJECT;
14
+ const task = env.HARNESS_TASK_ID ?? env.DEVCONTOUR_TASK_ID;
15
+ const run = env.HARNESS_RUN_ID ?? env.DEVCONTOUR_RUN_ID;
16
+ const phase = env.HARNESS_PHASE ?? env.DEVCONTOUR_PHASE;
17
+ const barrier = env.S03_BARRIER;
18
+ const value = `${task}:${run}:${phase}`;
19
+
20
+ const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8' }).trim();
21
+ const containerOf = (name) =>
22
+ docker(
23
+ 'ps',
24
+ '-q',
25
+ '--no-trunc',
26
+ '--filter',
27
+ `label=devcontour.harness.project=${name}`,
28
+ '--filter',
29
+ 'label=com.docker.compose.service=postgres',
30
+ );
31
+ const psql = (container, sql) =>
32
+ docker('exec', container, 'psql', '-U', env.POSTGRES_USER, '-d', env.POSTGRES_DB, '-tAc', sql)
33
+ .split('\n')
34
+ .filter(Boolean);
35
+ const volumesOf = (container) =>
36
+ JSON.parse(docker('inspect', '--format', '{{json .Mounts}}', container))
37
+ .map((m) => m.Name)
38
+ .filter(Boolean);
39
+ async function waitFile(path, timeoutMs) {
40
+ const deadline = Date.now() + timeoutMs;
41
+ while (!existsSync(path)) {
42
+ if (Date.now() > deadline) throw new Error(`барьер не наступил: ${path}`);
43
+ await new Promise((r) => setTimeout(r, 200));
44
+ }
45
+ return JSON.parse(readFileSync(path, 'utf8'));
46
+ }
47
+
48
+ const own = containerOf(project);
49
+
50
+ test('S03 db-isolation: своя база попытки принимает и возвращает значение', () => {
51
+ assert.ok(project && own, 'нет контейнера postgres проекта ' + project);
52
+ psql(own, 'CREATE TABLE IF NOT EXISTS marks (v text PRIMARY KEY)');
53
+ psql(own, `INSERT INTO marks VALUES ('${value}') ON CONFLICT DO NOTHING`);
54
+ assert.deepEqual(psql(own, 'SELECT v FROM marks ORDER BY v'), [value]);
55
+ });
56
+
57
+ if (phase === 'candidate' && barrier) {
58
+ const other = task === 's03-a' ? 's03-b' : 's03-a';
59
+ test('S03 db-isolation: база другой попытки не видит значение этой, и наоборот', async () => {
60
+ mkdirSync(barrier, { recursive: true });
61
+ const mine = {
62
+ task,
63
+ run,
64
+ project,
65
+ container: own,
66
+ port: Number(env.POSTGRES_PORT),
67
+ volumes: volumesOf(own),
68
+ value,
69
+ };
70
+ writeFileSync(join(barrier, `${task}.json`), JSON.stringify(mine, null, 2));
71
+ const theirs = await waitFile(join(barrier, `${other}.json`), 180_000);
72
+ assert.notEqual(theirs.project, mine.project, 'Docker project');
73
+ assert.notEqual(theirs.port, mine.port, 'порт');
74
+ assert.notDeepEqual(theirs.volumes, mine.volumes, 'тома');
75
+ // Обе стороны записали значения — сверка read-back после барьера.
76
+ writeFileSync(join(barrier, `${task}.written`), '{}');
77
+ await waitFile(join(barrier, `${other}.written`), 180_000);
78
+ const ownRows = psql(own, 'SELECT v FROM marks ORDER BY v');
79
+ const theirRows = psql(theirs.container, 'SELECT v FROM marks ORDER BY v');
80
+ writeFileSync(
81
+ join(barrier, `${task}.readback.json`),
82
+ JSON.stringify({ own: ownRows, other: theirRows }, null, 2),
83
+ );
84
+ assert.deepEqual(ownRows, [value], 'в своей базе только своё значение');
85
+ assert.ok(!theirRows.includes(value), 'значение этой попытки видно в чужой базе');
86
+ assert.ok(theirRows.includes(theirs.value), 'в чужой базе нет её значения');
87
+ });
88
+ if (task === 's03-b')
89
+ test('S03 db-isolation: после teardown попытки A база B цела', async () => {
90
+ // Сценарий подтверждает: окружение A удалено, контрольный ресурс жив.
91
+ const checked = await waitFile(join(barrier, 'a-teardown.checked.json'), 600_000);
92
+ assert.equal(checked.ok, true, JSON.stringify(checked));
93
+ const rows = psql(own, 'SELECT v FROM marks ORDER BY v');
94
+ writeFileSync(join(barrier, 's03-b.after-a-teardown.json'), JSON.stringify(rows));
95
+ assert.deepEqual(rows, [value]);
96
+ });
97
+ }
@@ -0,0 +1,75 @@
1
+ // Gate `env-probe` (S06): процесс проверки получает только объявленное окружение.
2
+ // Значения canary не выводятся; в каталог окружения пишутся только имена переменных.
3
+ // Если в дереве есть `s06/trigger-fail`, проверка намеренно падает с выводом секрета —
4
+ // harness обязан замаскировать его в логах, evidence и итоге.
5
+ // node .harness/compose-bridge.mjs exec -- node --test --test-reporter=junit \
6
+ // --test-reporter-destination=.artifacts/junit/env-probe.xml .harness/acceptance/env-probe.test.mjs
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ const env = process.env;
13
+ const dir = env.HARNESS_ENVIRONMENT_DIR ?? env.DEVCONTOUR_ENVIRONMENT_DIR;
14
+ /** Ключи, которые задаёт harness для шагов и gates (HARNESS_ — псевдонимы DEVCONTOUR_). */
15
+ const HARNESS_KEYS = [
16
+ 'REPOSITORY_ID',
17
+ 'RUN_ID',
18
+ 'TASK_ID',
19
+ 'PHASE',
20
+ 'RESOURCES_JSON',
21
+ 'DEPENDENCIES_JSON',
22
+ 'COMPONENTS_JSON',
23
+ 'ENVIRONMENT_DIR',
24
+ 'REPORT_PATH',
25
+ 'PROCESS_MARK',
26
+ ];
27
+ const BASE = ['PATH', 'HOME', 'TMPDIR', 'USER', 'LOGNAME', 'CI'];
28
+ /** Добавляют node --test и compose-bridge exec; macOS добавляет __CF_USER_TEXT_ENCODING каждому процессу. */
29
+ const TOOLING = ['NODE_TEST_CONTEXT', 'HX_PROJECT', 'HX_ENV_DIR', '__CF_USER_TEXT_ENCODING'];
30
+ /** Свежесть test-gate: harness отключает Turbo cache и кэш тестов Go (значения не секреты). */
31
+ const FRESHNESS = ['TURBO_FORCE', 'GOFLAGS'];
32
+
33
+ test('S06 env-probe: разрешённая переменная доступна', () => {
34
+ assert.match(env.S06_ALLOWED ?? '', /^canary-allowed-/);
35
+ });
36
+ test('S06 env-probe: неразрешённая переменная оператора отсутствует', () => {
37
+ assert.equal(env.S06_DISALLOWED, undefined);
38
+ assert.equal(env.DEVCONTOUR_OPERATOR_CANARY, undefined, 'чужая DEVCONTOUR_* переменная');
39
+ assert.equal(env.HARNESS_OPERATOR_CANARY, undefined, 'чужая HARNESS_* переменная');
40
+ });
41
+ test('S06 env-probe: секрет передан процессу по имени из политики', () => {
42
+ assert.match(env.HX_DB_PASSWORD ?? '', /^canary-secret-/);
43
+ assert.equal(env.POSTGRES_PASSWORD, env.HX_DB_PASSWORD, 'env-файл окружения');
44
+ });
45
+ test('S06 env-probe: окружение — только объявленное и совместимое HARNESS_/DEVCONTOUR_', () => {
46
+ const declared = (env.S06_DECLARED ?? '').split(',').filter(Boolean);
47
+ const envFile = (env.S06_ENV_FILE_KEYS ?? '').split(',').filter(Boolean);
48
+ const allowed = new Set([
49
+ ...BASE,
50
+ ...TOOLING,
51
+ ...FRESHNESS,
52
+ ...declared,
53
+ ...envFile,
54
+ ...HARNESS_KEYS.flatMap((k) => ['DEVCONTOUR_' + k, 'HARNESS_' + k]),
55
+ ]);
56
+ const keys = Object.keys(env).sort();
57
+ if (dir) {
58
+ mkdirSync(join(dir, 'probe'), { recursive: true });
59
+ writeFileSync(
60
+ join(dir, 'probe', `env-keys-${env.DEVCONTOUR_PHASE}.json`),
61
+ JSON.stringify(keys),
62
+ );
63
+ }
64
+ const extra = keys.filter((k) => !allowed.has(k));
65
+ assert.deepEqual(extra, [], 'необъявленные переменные: ' + extra.join(', '));
66
+ for (const k of ['RUN_ID', 'TASK_ID', 'PHASE', 'REPOSITORY_ID'])
67
+ assert.equal(env['HARNESS_' + k], env['DEVCONTOUR_' + k], 'псевдоним HARNESS_' + k);
68
+ });
69
+ if (existsSync('s06/trigger-fail'))
70
+ test('S06 env-probe: команда падает с выводом секрета (маскирование harness)', () => {
71
+ const secret = env.HX_DB_PASSWORD;
72
+ console.log('stdout leak attempt: ' + secret);
73
+ console.error('stderr leak attempt: ' + secret);
74
+ throw new Error('failure with secret ' + secret);
75
+ });