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,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
+ });
@@ -0,0 +1,54 @@
1
+ // Gate `sdk-generation` (S02): закоммиченные OpenAPI и SDK равны выходу закреплённого генератора
2
+ // на закоммиченных исходниках. Генератор запускается штатной командой starter; после
3
+ // сравнения рабочее дерево возвращается к проверяемому коммиту, чтобы gate не менял
4
+ // отслеживаемые файлы (harness проверяет это отдельно).
5
+ // node --test --test-reporter=junit --test-reporter-destination=<report> .harness/acceptance/generation.test.mjs
6
+ import { after, before, test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { execFileSync, spawnSync } from 'node:child_process';
9
+
10
+ /** Выходы генерации, которые поручение вправе менять только через генератор. */
11
+ export const GENERATED = [
12
+ 'apps/nest-backend/openapi',
13
+ 'packages/client-rest-api-sdk/src/generated',
14
+ 'packages/admin-rest-api-sdk/src/generated',
15
+ ];
16
+ /** Штатная команда генерации starter; версия codegen закреплена в package.json SDK (защищён). */
17
+ const GENERATOR = ['pnpm', 'rest-api-sdk:generate'];
18
+
19
+ const root = process.env.FIXTURE_ROOT ?? process.cwd();
20
+ const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8' });
21
+ let generation;
22
+
23
+ before(() => {
24
+ // Детерминированный вход: без .env файлов starter и без сетевого update-check Prisma.
25
+ generation = spawnSync(GENERATOR[0], GENERATOR.slice(1), {
26
+ cwd: root,
27
+ encoding: 'utf8',
28
+ env: { ...process.env, DOTENV_CONFIG_PATH: '/dev/null', CHECKPOINT_DISABLE: '1' },
29
+ timeout: 540_000,
30
+ });
31
+ });
32
+
33
+ after(() => {
34
+ // Вернуть дерево к проверяемому коммиту: только пути генерации.
35
+ git('checkout', '--', ...GENERATED);
36
+ git('clean', '-fdq', '--', ...GENERATED);
37
+ });
38
+
39
+ test('sdk-generation: генератор завершился успешно', () => {
40
+ assert.equal(
41
+ generation.status,
42
+ 0,
43
+ `${GENERATOR.join(' ')}: exit ${generation.status}\n${(generation.stderr || generation.stdout).slice(-4000)}`,
44
+ );
45
+ });
46
+
47
+ for (const path of GENERATED)
48
+ test(`sdk-generation: ${path} совпадает с выходом генератора`, () => {
49
+ assert.equal(generation.status, 0, 'генерация не выполнена');
50
+ const diff = git('diff', '--stat', '--', path).trim();
51
+ const untracked = git('ls-files', '--others', '--exclude-standard', '--', path).trim();
52
+ assert.equal(diff, '', `устаревший или изменённый вручную выход генерации:\n${diff}`);
53
+ assert.equal(untracked, '', `генератор создал незакоммиченные файлы:\n${untracked}`);
54
+ });
@@ -0,0 +1,43 @@
1
+ // Приёмочные проверки поручения `issued-at` (S01): контракт backend → OpenAPI → SDK → потребитель.
2
+ // Поставляются примером devcontour-harness и запускаются gate `acceptance`:
3
+ // node --test --test-reporter=junit --test-reporter-destination=<report> .harness/acceptance/issued-at.test.mjs
4
+ // Только встроенные модули Node: зависимостей проекта не требуется.
5
+ import { test } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+
10
+ const root = process.env.FIXTURE_ROOT ?? process.cwd();
11
+ const read = (path) => readFileSync(resolve(root, path), 'utf8');
12
+
13
+ /** Тело `export interface <name> { ... }` в сгенерированном файле SDK. */
14
+ function interfaceBody(source, name) {
15
+ const start = source.indexOf(`export interface ${name} {`);
16
+ assert.ok(start >= 0, `в SDK нет interface ${name}`);
17
+ return source.slice(start, source.indexOf('\n}', start));
18
+ }
19
+
20
+ for (const realm of ['client', 'admin']) {
21
+ test(`issued-at: ${realm} OpenAPI AccessTokenDto.issuedAt — обязательный date-time`, () => {
22
+ const doc = JSON.parse(read(`apps/nest-backend/openapi/${realm}.openapi.json`));
23
+ const schema = doc.components.schemas.AccessTokenDto;
24
+ assert.equal(schema.properties.issuedAt?.type, 'string');
25
+ assert.equal(schema.properties.issuedAt?.format, 'date-time');
26
+ assert.ok(schema.required.includes('issuedAt'), 'issuedAt должен быть required');
27
+ });
28
+ test(`issued-at: ${realm} SDK AccessTokenDto объявляет issuedAt: string`, () => {
29
+ const body = interfaceBody(
30
+ read(`packages/${realm}-rest-api-sdk/src/generated/data-contracts.ts`),
31
+ 'AccessTokenDto',
32
+ );
33
+ assert.match(body, /\n\s+issuedAt: string;/);
34
+ });
35
+ }
36
+
37
+ test('issued-at: потребитель web использует AccessTokenDto.issuedAt из SDK', () => {
38
+ const path = 'apps/next-web-app/src/infra/client-api/types/access-token-issued-at.type.ts';
39
+ assert.ok(existsSync(resolve(root, path)), `нет ${path}`);
40
+ const text = read(path);
41
+ assert.match(text, /from '@oim\/client-rest-api-sdk\/data-contracts'/);
42
+ assert.match(text, /AccessTokenDto\['issuedAt'\]/);
43
+ });
@@ -0,0 +1,47 @@
1
+ // Gate `lifecycle-probe` (S04): отмечает момент старта gate в каталоге окружения фазы
2
+ // и ведёт себя по файлу режима S04_GATE ({"mode":"pass"|"fail"|"block"}):
3
+ // pass — успех; fail — отказ проверки; block — ждать, пока процесс harness не исчезнет
4
+ // (сценарий прерывает его SIGKILL при активном окружении).
5
+ // node --test --test-reporter=junit --test-reporter-destination=.artifacts/junit/lifecycle.xml \
6
+ // .harness/acceptance/lifecycle-probe.test.mjs
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { existsSync, readFileSync, 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
+ const mode =
15
+ env.S04_GATE && existsSync(env.S04_GATE)
16
+ ? JSON.parse(readFileSync(env.S04_GATE, 'utf8')).mode
17
+ : 'pass';
18
+
19
+ test('S04 lifecycle-probe: gate стартует только в готовом окружении', async () => {
20
+ assert.ok(dir, 'нет каталога окружения фазы');
21
+ const receipt = JSON.parse(readFileSync(join(dir, 'bridge', 'receipt.json'), 'utf8'));
22
+ writeFileSync(
23
+ join(dir, 'gate-started.json'),
24
+ JSON.stringify({
25
+ at: new Date().toISOString(),
26
+ pid: process.pid,
27
+ bridge: receipt.status,
28
+ mode,
29
+ }),
30
+ );
31
+ assert.equal(receipt.status, 'ready', 'gate до ready');
32
+ if (mode === 'block') {
33
+ // Процесс harness — владелец окружения из его receipt. Его исчезновение и есть сигнал.
34
+ const owner = JSON.parse(readFileSync(join(dir, 'environment.json'), 'utf8')).ownerPid;
35
+ const alive = () => {
36
+ try {
37
+ process.kill(owner, 0);
38
+ return true;
39
+ } catch (error) {
40
+ return error.code === 'EPERM';
41
+ }
42
+ };
43
+ while (alive()) await new Promise((r) => setTimeout(r, 200));
44
+ assert.fail('процесс harness прерван');
45
+ }
46
+ assert.notEqual(mode, 'fail', 'проверка намеренно провалена (S04 gate failure)');
47
+ });