@agilno-tech/rivet 0.1.0-alpha.0

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 (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,727 @@
1
+ import {
2
+ closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync,
3
+ realpathSync, renameSync, rmSync, statSync, writeFileSync,
4
+ } from 'node:fs';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { extname, isAbsolute, join, resolve, sep } from 'node:path';
7
+ import { inflateSync } from 'node:zlib';
8
+
9
+ import { containsSecretMaterial } from '../clients/contract.js';
10
+ import { assertGitClient } from '../git/client.js';
11
+ import { claimApproval } from '../policy/approvals.js';
12
+ import { assertQualityRun } from '../quality/runner.js';
13
+ import { assertTraceabilityResult } from '../quality/traceability.js';
14
+ import { sha256 } from './checksum.js';
15
+ import { validateEvidenceManifest } from './validate.js';
16
+
17
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
18
+ const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9]|lpt[1-9])(?:\..*)?$/i;
19
+ const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
20
+ const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
21
+
22
+ export class EvidenceCollectionError extends Error {
23
+ constructor(reason = 'invalid-evidence-input') {
24
+ const messages = {
25
+ 'invalid-evidence-input': 'Evidence collection input is invalid.',
26
+ 'unsafe-path': 'Evidence artifact path is unsafe.',
27
+ 'approval-required': 'Exact human evidence approval is required.',
28
+ 'publication-invalid': 'Durable publication requires an approved HTTPS URL and exact archive checksum.',
29
+ 'bundle-exists': 'Evidence run already exists and cannot be overwritten.',
30
+ 'bundle-write-failed': 'Evidence bundle could not be written safely.',
31
+ };
32
+ super(messages[reason] ?? messages['invalid-evidence-input']);
33
+ this.name = 'EvidenceCollectionError';
34
+ this.code = 'ERR_EVIDENCE_COLLECTION';
35
+ this.safeMessage = this.message;
36
+ this.details = Object.freeze({ reason });
37
+ }
38
+ }
39
+
40
+ function fail(reason) { throw new EvidenceCollectionError(reason); }
41
+
42
+ function capture(value, allowed, required = []) {
43
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-evidence-input');
44
+ let keys;
45
+ try { keys = Reflect.ownKeys(value); } catch { fail('invalid-evidence-input'); }
46
+ if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-evidence-input');
47
+ const result = Object.create(null);
48
+ for (const key of keys) {
49
+ let descriptor;
50
+ try { descriptor = Object.getOwnPropertyDescriptor(value, key); } catch { fail('invalid-evidence-input'); }
51
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-evidence-input');
52
+ result[key] = descriptor.value;
53
+ }
54
+ if (required.some(key => !Object.hasOwn(result, key))) fail('invalid-evidence-input');
55
+ return result;
56
+ }
57
+
58
+ function array(value, maximum, convert) {
59
+ try {
60
+ if (!Array.isArray(value)) fail('invalid-evidence-input');
61
+ const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
62
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximum
63
+ || Reflect.ownKeys(value).length !== length + 1) fail('invalid-evidence-input');
64
+ const result = [];
65
+ for (let index = 0; index < length; index += 1) {
66
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
67
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-evidence-input');
68
+ result.push(convert(descriptor.value));
69
+ }
70
+ return result;
71
+ } catch (error) {
72
+ if (error instanceof EvidenceCollectionError) throw error;
73
+ fail('invalid-evidence-input');
74
+ }
75
+ }
76
+
77
+ function id(value) {
78
+ if (typeof value !== 'string' || value.length > 64 || !ID.test(value)) fail('invalid-evidence-input');
79
+ return value;
80
+ }
81
+
82
+ function relativePath(value) {
83
+ if (typeof value !== 'string' || value.length < 1 || value.length > 500
84
+ || isAbsolute(value) || value.normalize('NFKC') !== value
85
+ || /[\\:\u0000-\u001f\u007f]/.test(value) || value.startsWith('-')
86
+ || value.endsWith('/') || value.includes('//')) fail('unsafe-path');
87
+ const parts = value.split('/');
88
+ if (parts.some(part => !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' ')
89
+ || WINDOWS_RESERVED.test(part) || part.toUpperCase().toLowerCase() === '.git')) fail('unsafe-path');
90
+ return value;
91
+ }
92
+
93
+ function immutable(value) {
94
+ if (Array.isArray(value)) return Object.freeze(value.map(immutable));
95
+ if (value && typeof value === 'object') {
96
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, immutable(child)])));
97
+ }
98
+ return value;
99
+ }
100
+
101
+ function jsonSnapshot(value) {
102
+ let seen = 0;
103
+ const active = new WeakSet();
104
+ function visit(child, depth) {
105
+ if (depth > 32 || ++seen > 100_000) fail('invalid-evidence-input');
106
+ if (child === null || typeof child === 'boolean') return child;
107
+ if (typeof child === 'number') {
108
+ if (!Number.isFinite(child)) fail('invalid-evidence-input');
109
+ return child;
110
+ }
111
+ if (typeof child === 'string') {
112
+ if (child.length > 16_384 || containsSecretMaterial(child)) fail('invalid-evidence-input');
113
+ return child;
114
+ }
115
+ if (!child || typeof child !== 'object' || active.has(child)) fail('invalid-evidence-input');
116
+ active.add(child);
117
+ try {
118
+ if (Array.isArray(child)) return array(child, 10_000, item => visit(item, depth + 1));
119
+ const prototype = Object.getPrototypeOf(child);
120
+ if (prototype !== Object.prototype && prototype !== null) fail('invalid-evidence-input');
121
+ const keys = Reflect.ownKeys(child);
122
+ if (keys.length > 10_000 || keys.some(key => typeof key !== 'string')) fail('invalid-evidence-input');
123
+ const output = {};
124
+ for (const key of keys) {
125
+ const descriptor = Object.getOwnPropertyDescriptor(child, key);
126
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-evidence-input');
127
+ output[key] = visit(descriptor.value, depth + 1);
128
+ }
129
+ return output;
130
+ } finally { active.delete(child); }
131
+ }
132
+ try { return visit(value, 0); }
133
+ catch (error) {
134
+ if (error instanceof EvidenceCollectionError) throw error;
135
+ fail('invalid-evidence-input');
136
+ }
137
+ }
138
+
139
+ function sameIdentity(left, right) { return left.dev === right.dev && left.ino === right.ino; }
140
+
141
+ function canonicalDirectory(path) {
142
+ if (typeof path !== 'string' || !isAbsolute(path) || resolve(path) !== path || path.length > 1_024) fail('unsafe-path');
143
+ try {
144
+ const before = lstatSync(path, { bigint: true });
145
+ const canonical = realpathSync(path);
146
+ const after = lstatSync(path, { bigint: true });
147
+ if (!before.isDirectory() || before.isSymbolicLink() || !after.isDirectory() || after.isSymbolicLink()
148
+ || !sameIdentity(before, after) || canonical !== path) fail('unsafe-path');
149
+ return Object.freeze({ path, dev: after.dev, ino: after.ino });
150
+ } catch (error) {
151
+ if (error instanceof EvidenceCollectionError) throw error;
152
+ fail('unsafe-path');
153
+ }
154
+ }
155
+
156
+ function withPinnedDirectory(directory, expected, operation) {
157
+ let original;
158
+ try { original = process.cwd(); } catch { fail('bundle-write-failed'); }
159
+ let changed = false;
160
+ let result;
161
+ let primary = null;
162
+ try {
163
+ process.chdir(directory);
164
+ changed = true;
165
+ const pinned = statSync('.', { bigint: true });
166
+ if (!pinned.isDirectory() || !sameIdentity(pinned, expected)) fail('unsafe-path');
167
+ result = operation();
168
+ } catch (error) {
169
+ primary = error;
170
+ }
171
+ if (changed) {
172
+ try { process.chdir(original); }
173
+ catch (error) { if (primary === null) primary = error; }
174
+ }
175
+ if (primary !== null) {
176
+ if (primary instanceof EvidenceCollectionError) throw primary;
177
+ fail('bundle-write-failed');
178
+ }
179
+ return result;
180
+ }
181
+
182
+ function readPinnedArtifact(project, relative) {
183
+ const components = relative.split('/');
184
+ return withPinnedDirectory(project.path, project, () => {
185
+ for (const component of components.slice(0, -1)) {
186
+ const before = lstatSync(component, { bigint: true });
187
+ if (!before.isDirectory() || before.isSymbolicLink()) fail('unsafe-path');
188
+ process.chdir(component);
189
+ const pinned = statSync('.', { bigint: true });
190
+ if (!sameIdentity(before, pinned)) fail('unsafe-path');
191
+ }
192
+ const name = components.at(-1);
193
+ const before = lstatSync(name, { bigint: true });
194
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n
195
+ || before.size < 0n || before.size > BigInt(MAX_TOTAL_BYTES)) fail('unsafe-path');
196
+ let descriptor;
197
+ try {
198
+ descriptor = openSync(name, constants.O_RDONLY | NOFOLLOW);
199
+ const opened = fstatSync(descriptor, { bigint: true });
200
+ if (!opened.isFile() || opened.nlink !== 1n || !sameIdentity(before, opened)) fail('unsafe-path');
201
+ const bytes = readFileSync(descriptor);
202
+ const after = fstatSync(descriptor, { bigint: true });
203
+ if (!sameIdentity(opened, after) || after.size !== BigInt(bytes.byteLength)) fail('unsafe-path');
204
+ return Object.freeze({ bytes, size: bytes.byteLength, sha256: sha256(bytes) });
205
+ } finally { if (descriptor !== undefined) closeSync(descriptor); }
206
+ });
207
+ }
208
+
209
+ function scanSecrets(bytes) {
210
+ const chunkBytes = 512 * 1024;
211
+ const overlap = 4 * 1024;
212
+ for (let offset = 0; offset < bytes.length; offset += chunkBytes - overlap) {
213
+ const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + chunkBytes));
214
+ if (containsSecretMaterial(chunk.toString('latin1')) || containsSecretMaterial(chunk.toString('utf8'))) {
215
+ fail('invalid-evidence-input');
216
+ }
217
+ }
218
+ }
219
+
220
+ function crc32(bytes) {
221
+ let crc = 0xffffffff;
222
+ for (const byte of bytes) {
223
+ crc ^= byte;
224
+ for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
225
+ }
226
+ return (crc ^ 0xffffffff) >>> 0;
227
+ }
228
+
229
+ function pngPasses(width, height, interlace) {
230
+ if (interlace === 0) return [[width, height]];
231
+ const passes = [[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2]];
232
+ return passes.map(([x, y, dx, dy]) => [
233
+ width <= x ? 0 : Math.ceil((width - x) / dx),
234
+ height <= y ? 0 : Math.ceil((height - y) / dy),
235
+ ]);
236
+ }
237
+
238
+ function safePngText(type, data) {
239
+ try {
240
+ if (type === 'tEXt') {
241
+ const separator = data.indexOf(0);
242
+ if (separator < 1 || separator > 79) return false;
243
+ scanSecrets(data);
244
+ return true;
245
+ }
246
+ if (type === 'zTXt') {
247
+ const separator = data.indexOf(0);
248
+ if (separator < 1 || separator > 79 || separator + 2 > data.length || data[separator + 1] !== 0) return false;
249
+ scanSecrets(data.subarray(0, separator));
250
+ const decoded = inflateSync(data.subarray(separator + 2), { maxOutputLength: 1024 * 1024 });
251
+ scanSecrets(decoded);
252
+ return true;
253
+ }
254
+ if (type === 'iTXt') {
255
+ const keywordEnd = data.indexOf(0);
256
+ if (keywordEnd < 1 || keywordEnd > 79 || keywordEnd + 3 > data.length || ![0, 1].includes(data[keywordEnd + 1])
257
+ || data[keywordEnd + 2] !== 0) return false;
258
+ const languageStart = keywordEnd + 3;
259
+ const languageEnd = data.indexOf(0, languageStart);
260
+ const translatedEnd = languageEnd < 0 ? -1 : data.indexOf(0, languageEnd + 1);
261
+ if (languageEnd < 0 || translatedEnd < 0) return false;
262
+ scanSecrets(data.subarray(0, translatedEnd + 1));
263
+ const encoded = data.subarray(translatedEnd + 1);
264
+ const decoded = data[keywordEnd + 1] === 1
265
+ ? inflateSync(encoded, { maxOutputLength: 1024 * 1024 }) : encoded;
266
+ scanSecrets(decoded);
267
+ return true;
268
+ }
269
+ return true;
270
+ } catch (error) {
271
+ if (error instanceof EvidenceCollectionError) throw error;
272
+ return false;
273
+ }
274
+ }
275
+
276
+ function safePngProfile(data) {
277
+ try {
278
+ const separator = data.indexOf(0);
279
+ if (separator < 1 || separator > 79 || separator + 2 > data.length || data[separator + 1] !== 0) return false;
280
+ scanSecrets(data.subarray(0, separator));
281
+ const decoded = inflateSync(data.subarray(separator + 2), { maxOutputLength: 1024 * 1024 });
282
+ if (decoded.length === 0) return false;
283
+ scanSecrets(decoded);
284
+ return true;
285
+ } catch (error) {
286
+ if (error instanceof EvidenceCollectionError) throw error;
287
+ return false;
288
+ }
289
+ }
290
+
291
+ function validPng(bytes) {
292
+ const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
293
+ if (bytes.length < 45 || !bytes.subarray(0, 8).equals(signature)) return false;
294
+ let offset = 8;
295
+ let first = true;
296
+ let header = null;
297
+ let sawIdat = false;
298
+ let idatEnded = false;
299
+ let sawPlte = false;
300
+ let sawProfile = false;
301
+ const idat = [];
302
+ const safeBinaryAncillary = new Set(['cHRM', 'gAMA', 'sBIT', 'sRGB', 'bKGD', 'hIST', 'tRNS', 'pHYs', 'tIME']);
303
+ while (offset + 12 <= bytes.length) {
304
+ const length = bytes.readUInt32BE(offset);
305
+ if (length > MAX_TOTAL_BYTES || offset + 12 + length > bytes.length) return false;
306
+ const type = bytes.toString('ascii', offset + 4, offset + 8);
307
+ if (!/^[A-Za-z]{4}$/.test(type) || (first && (type !== 'IHDR' || length !== 13))) return false;
308
+ const data = bytes.subarray(offset + 8, offset + 8 + length);
309
+ if (crc32(bytes.subarray(offset + 4, offset + 8 + length)) !== bytes.readUInt32BE(offset + 8 + length)) return false;
310
+ if (sawIdat && type !== 'IDAT' && type !== 'IEND') idatEnded = true;
311
+ if (type === 'IHDR') {
312
+ if (!first || header !== null) return false;
313
+ const width = data.readUInt32BE(0);
314
+ const height = data.readUInt32BE(4);
315
+ const bitDepth = data[8];
316
+ const colorType = data[9];
317
+ const validDepths = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] };
318
+ if (!width || !height || width > 10_000 || height > 10_000 || !validDepths[colorType]?.includes(bitDepth)
319
+ || data[10] !== 0 || data[11] !== 0 || ![0, 1].includes(data[12])) return false;
320
+ header = { width, height, bitDepth, colorType, interlace: data[12] };
321
+ } else if (type === 'PLTE') {
322
+ if (!header || sawPlte || sawIdat || [0, 4].includes(header.colorType)
323
+ || length < 3 || length > 768 || length % 3 !== 0
324
+ || (header.colorType === 3 && length / 3 > 2 ** header.bitDepth)) return false;
325
+ sawPlte = true;
326
+ } else if (['tEXt', 'zTXt', 'iTXt'].includes(type)) {
327
+ if (!safePngText(type, data)) return false;
328
+ } else if (type === 'iCCP') {
329
+ if (sawProfile || sawIdat || !safePngProfile(data)) return false;
330
+ sawProfile = true;
331
+ } else if (type === 'IDAT') {
332
+ if (!header || idatEnded || (header.colorType === 3 && !sawPlte)) return false;
333
+ sawIdat = true;
334
+ idat.push(data);
335
+ } else if (type !== 'IEND') {
336
+ const ancillary = (type.charCodeAt(0) & 0x20) !== 0;
337
+ if (!ancillary || !safeBinaryAncillary.has(type)) return false;
338
+ }
339
+ offset += 12 + length;
340
+ first = false;
341
+ if (type === 'IEND') {
342
+ if (length !== 0 || offset !== bytes.length || !header || !sawIdat) return false;
343
+ const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[header.colorType];
344
+ const bitsPerPixel = channels * header.bitDepth;
345
+ const passes = pngPasses(header.width, header.height, header.interlace);
346
+ const expected = passes.reduce((total, [width, height]) => total
347
+ + (width === 0 || height === 0 ? 0 : height * (1 + Math.ceil(width * bitsPerPixel / 8))), 0);
348
+ if (expected <= 0 || expected > MAX_TOTAL_BYTES) return false;
349
+ let decoded;
350
+ try { decoded = inflateSync(Buffer.concat(idat), { maxOutputLength: expected + 1 }); }
351
+ catch { return false; }
352
+ if (decoded.length !== expected) return false;
353
+ let decodedOffset = 0;
354
+ for (const [width, height] of passes) {
355
+ if (width === 0 || height === 0) continue;
356
+ const rowBytes = Math.ceil(width * bitsPerPixel / 8);
357
+ for (let row = 0; row < height; row += 1) {
358
+ if (decoded[decodedOffset] > 4) return false;
359
+ decodedOffset += 1 + rowBytes;
360
+ }
361
+ }
362
+ return decodedOffset === decoded.length;
363
+ }
364
+ }
365
+ return false;
366
+ }
367
+
368
+ function validImage(bytes, extension) {
369
+ return extension === '.png' && validPng(bytes);
370
+ }
371
+
372
+ function artifact(value) {
373
+ const input = capture(value, new Set(['id', 'type', 'path', 'testId']), ['id', 'type', 'path', 'testId']);
374
+ if (!['screenshot', 'playwright-report', 'trace', 'report'].includes(input.type)) fail('invalid-evidence-input');
375
+ return Object.freeze({ id: id(input.id), type: input.type, path: relativePath(input.path), testId: id(input.testId) });
376
+ }
377
+
378
+ function reviewInput(value) {
379
+ const input = capture(value, new Set([
380
+ 'id', 'reviewerId', 'required', 'expectedApproverId', 'approvalFor',
381
+ ]), ['id', 'reviewerId', 'required', 'expectedApproverId', 'approvalFor']);
382
+ if (typeof input.required !== 'boolean' || typeof input.approvalFor !== 'function') fail('invalid-evidence-input');
383
+ const reviewerId = id(input.reviewerId);
384
+ const expectedApproverId = id(input.expectedApproverId);
385
+ if (reviewerId !== expectedApproverId) fail('invalid-evidence-input');
386
+ return Object.freeze({ id: id(input.id), reviewerId, required: input.required, expectedApproverId, approvalFor: input.approvalFor });
387
+ }
388
+
389
+ function approvalInput(value) {
390
+ const input = capture(value, new Set(['expectedApproverId', 'approvalFor']), ['expectedApproverId', 'approvalFor']);
391
+ if (typeof input.approvalFor !== 'function') fail('invalid-evidence-input');
392
+ return Object.freeze({ expectedApproverId: id(input.expectedApproverId), approvalFor: input.approvalFor });
393
+ }
394
+
395
+ function publicationInput(value) {
396
+ if (value === undefined) return null;
397
+ let input;
398
+ try {
399
+ input = capture(value, new Set(['remoteUrl', 'expectedApproverId', 'approvalFor']), ['remoteUrl', 'expectedApproverId', 'approvalFor']);
400
+ } catch { fail('publication-invalid'); }
401
+ let parsed;
402
+ try { parsed = new URL(input.remoteUrl); } catch { fail('publication-invalid'); }
403
+ if (typeof input.remoteUrl !== 'string' || input.remoteUrl.length > 2_048
404
+ || parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.search || parsed.hash
405
+ || typeof input.approvalFor !== 'function') fail('publication-invalid');
406
+ return Object.freeze({ remoteUrl: input.remoteUrl, expectedApproverId: id(input.expectedApproverId), approvalFor: input.approvalFor });
407
+ }
408
+
409
+ function claimExact(receipt, request, registry, expectedApproverId, nowMs, requireHuman, reason) {
410
+ let claim;
411
+ try {
412
+ claim = claimApproval(receipt, request, {
413
+ registry, expectedApproverId, requireHumanApprover: requireHuman, requireSingleUse: true, nowMs,
414
+ });
415
+ } catch { fail(reason); }
416
+ if (!claim.valid) fail(reason);
417
+ return claim;
418
+ }
419
+
420
+ function invokeApproval(provider, binding, reason) {
421
+ try { return provider(binding); } catch { fail(reason); }
422
+ }
423
+
424
+ function canonicalEvidenceLedger({ runId, graphId, commitSha, createdAt, traceability, prepared, reviews, finalReceipt }) {
425
+ const criteria = [...new Set(traceability.coverage.map(item => item.acceptanceCriterion))].sort();
426
+ const base = (itemId, type, requirementRefs, producer) => ({
427
+ id: itemId, type, requirementRefs, source: { commit: commitSha }, producer, timestamp: createdAt,
428
+ classification: 'internal-redacted', approvalState: 'approved',
429
+ });
430
+ const items = [base('commit', 'commit', criteria, { role: 'system', id: 'quality-runner' })];
431
+ const testCoverage = new Map();
432
+ for (const coverage of traceability.coverage) {
433
+ if (coverage.method === 'test') {
434
+ const requirements = testCoverage.get(coverage.evidenceId) ?? [];
435
+ requirements.push(coverage.acceptanceCriterion);
436
+ testCoverage.set(coverage.evidenceId, requirements);
437
+ } else {
438
+ items.push(base(coverage.evidenceId, 'review', [coverage.acceptanceCriterion], { role: 'human', id: coverage.approverId }));
439
+ items.push({
440
+ ...base(coverage.approvalReceiptId, 'human-approval', [coverage.acceptanceCriterion], { role: 'human', id: coverage.approverId }),
441
+ approval: { actorId: coverage.approverId, decision: 'approved' },
442
+ });
443
+ }
444
+ }
445
+ for (const [evidenceId, requirementRefs] of testCoverage) {
446
+ items.push({
447
+ ...base(evidenceId, 'test', [...new Set(requirementRefs)].sort(), { role: 'system', id: 'quality-runner' }),
448
+ details: { command: 'test', status: 'passed' },
449
+ });
450
+ }
451
+ for (const { item, contents, destination } of prepared) {
452
+ const requirementRefs = traceability.coverage.filter(entry => entry.evidenceId === item.testId).map(entry => entry.acceptanceCriterion);
453
+ items.push({
454
+ ...base(item.id, item.type === 'screenshot' ? 'screenshot' : 'report', requirementRefs, { role: 'system', id: 'quality-runner' }),
455
+ location: destination, checksum: 'sha256:' + contents.sha256,
456
+ });
457
+ }
458
+ for (const review of reviews) items.push(base(review.id, 'review', criteria, { role: 'manager', id: review.reviewerId }));
459
+ items.push({
460
+ ...base(finalReceipt.id, 'human-approval', criteria, { role: 'human', id: finalReceipt.approverId }),
461
+ approval: { actorId: finalReceipt.approverId, decision: 'approved' },
462
+ });
463
+ return { schemaVersion: 1, id: runId, graphId, approvalState: 'approved', publicationState: 'draft', items: items.sort((a, b) => a.id.localeCompare(b.id)) };
464
+ }
465
+
466
+ function writeExclusive(path, bytes) {
467
+ let descriptor;
468
+ try {
469
+ descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o400);
470
+ writeFileSync(descriptor, bytes);
471
+ fsyncSync(descriptor);
472
+ } finally { if (descriptor !== undefined) closeSync(descriptor); }
473
+ }
474
+
475
+ function assertPublicDirectory(output) {
476
+ let metadata;
477
+ let canonical;
478
+ try {
479
+ metadata = lstatSync(output.path, { bigint: true });
480
+ canonical = realpathSync(output.path);
481
+ } catch { fail('bundle-write-failed'); }
482
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || !sameIdentity(metadata, output)
483
+ || canonical !== output.path) fail('bundle-write-failed');
484
+ }
485
+
486
+ function publishAtomically(output, runId, files, claims, manifest) {
487
+ return withPinnedDirectory(output.path, output, () => {
488
+ const stageName = '.' + runId + '.stage-' + randomUUID();
489
+ let stageIdentity;
490
+ let published = false;
491
+ let finalized = false;
492
+ let insideStage = false;
493
+ try {
494
+ try { lstatSync(runId); fail('bundle-exists'); } catch (error) {
495
+ if (error instanceof EvidenceCollectionError) throw error;
496
+ if (error?.code !== 'ENOENT') fail('bundle-write-failed');
497
+ }
498
+ mkdirSync(stageName, { mode: 0o700 });
499
+ stageIdentity = lstatSync(stageName, { bigint: true });
500
+ if (!stageIdentity.isDirectory() || stageIdentity.isSymbolicLink()) fail('bundle-write-failed');
501
+ process.chdir(stageName);
502
+ insideStage = true;
503
+ const pinnedStage = statSync('.', { bigint: true });
504
+ if (!sameIdentity(stageIdentity, pinnedStage)) fail('bundle-write-failed');
505
+ mkdirSync('artifacts', { mode: 0o700 });
506
+ for (const file of files) writeExclusive(file.path, file.bytes);
507
+ process.chdir('..');
508
+ insideStage = false;
509
+ const rootAgain = statSync('.', { bigint: true });
510
+ if (!sameIdentity(rootAgain, output)) fail('bundle-write-failed');
511
+ const stageAgain = lstatSync(stageName, { bigint: true });
512
+ if (!sameIdentity(stageIdentity, stageAgain) || !stageAgain.isDirectory() || stageAgain.isSymbolicLink()) fail('bundle-write-failed');
513
+ assertPublicDirectory(output);
514
+ const persistedValidation = validateEvidenceManifest(manifest, { runDirectory: join(output.path, stageName) });
515
+ if (!persistedValidation.valid) fail('bundle-write-failed');
516
+ for (const claim of claims) if (!claim.finalize()) fail('bundle-write-failed');
517
+ finalized = true;
518
+ renameSync(stageName, runId);
519
+ published = true;
520
+ const finalIdentity = lstatSync(runId, { bigint: true });
521
+ if (!sameIdentity(stageIdentity, finalIdentity) || !finalIdentity.isDirectory() || finalIdentity.isSymbolicLink()) fail('bundle-write-failed');
522
+ assertPublicDirectory(output);
523
+ for (const claim of claims) if (!claim.publish()) fail('bundle-write-failed');
524
+ return join(output.path, runId);
525
+ } catch (error) {
526
+ if (insideStage) {
527
+ try { process.chdir('..'); } catch {}
528
+ insideStage = false;
529
+ }
530
+ if (finalized) for (const claim of claims) claim.rollback();
531
+ else for (const claim of claims) claim.release();
532
+ const residue = published ? runId : stageName;
533
+ try {
534
+ const current = lstatSync(residue, { bigint: true });
535
+ if (stageIdentity && sameIdentity(current, stageIdentity) && current.isDirectory() && !current.isSymbolicLink()) {
536
+ rmSync(residue, { recursive: true });
537
+ }
538
+ } catch {}
539
+ if (error instanceof EvidenceCollectionError) throw error;
540
+ fail('bundle-write-failed');
541
+ }
542
+ });
543
+ }
544
+
545
+ export async function collectEvidenceBundle(input) {
546
+ const value = capture(input, new Set([
547
+ 'projectRoot', 'outputRoot', 'runId', 'graphId', 'subjectId', 'approvalRegistry', 'nowMs', 'gitClient',
548
+ 'qualityRun', 'traceability', 'contexts', 'artifacts', 'reviews', 'finalApproval', 'publication',
549
+ ]), [
550
+ 'projectRoot', 'outputRoot', 'runId', 'graphId', 'subjectId', 'approvalRegistry', 'nowMs', 'gitClient',
551
+ 'qualityRun', 'traceability', 'contexts', 'artifacts', 'reviews', 'finalApproval',
552
+ ]);
553
+ const runId = id(value.runId);
554
+ const graphId = id(value.graphId);
555
+ const subjectId = id(value.subjectId);
556
+ if (!Number.isSafeInteger(value.nowMs) || value.nowMs < 0) fail('invalid-evidence-input');
557
+ try {
558
+ assertGitClient(value.gitClient);
559
+ assertQualityRun(value.qualityRun);
560
+ assertTraceabilityResult(value.traceability);
561
+ } catch { fail('invalid-evidence-input'); }
562
+ if (!value.traceability.valid || value.traceability.commitSha !== value.qualityRun.commitSha
563
+ || value.qualityRun.status !== 'pass') fail('invalid-evidence-input');
564
+
565
+ const project = canonicalDirectory(value.projectRoot);
566
+ const output = canonicalDirectory(value.outputRoot);
567
+ const projectAlias = project.path.split(sep).map(part => part.normalize('NFKC').toLowerCase()).join(sep);
568
+ const outputAlias = output.path.split(sep).map(part => part.normalize('NFKC').toLowerCase()).join(sep);
569
+ if (outputAlias === projectAlias || outputAlias.startsWith(projectAlias + sep)) fail('unsafe-path');
570
+ if (value.qualityRun.projectRoot !== project.path) fail('invalid-evidence-input');
571
+ let repository;
572
+ try { repository = await value.gitClient.inspectRepository(project.path); } catch { fail('invalid-evidence-input'); }
573
+ if (repository.root !== project.path || repository.headSha !== value.qualityRun.commitSha
574
+ || repository.dirty || repository.dirtyPaths.length !== 0) fail('invalid-evidence-input');
575
+
576
+ const contexts = jsonSnapshot(value.contexts);
577
+ const artifacts = array(value.artifacts, 256, artifact);
578
+ const reviewsInput = array(value.reviews, 256, reviewInput);
579
+ if (artifacts.length === 0 || reviewsInput.length === 0
580
+ || !reviewsInput.some(review => review.required)
581
+ || new Set(artifacts.map(item => item.id.toLowerCase())).size !== artifacts.length
582
+ || new Set(reviewsInput.map(item => item.id.toLowerCase())).size !== reviewsInput.length) fail('invalid-evidence-input');
583
+ const finalApproval = approvalInput(value.finalApproval);
584
+ const publication = publicationInput(value.publication);
585
+
586
+ const gateArtifacts = new Map();
587
+ for (const gate of value.qualityRun.gates) {
588
+ if (gate.status !== 'passed') continue;
589
+ for (const gateArtifact of gate.artifacts) {
590
+ if (gateArtifact.status === 'missing-or-unsafe' || gateArtifacts.has(gateArtifact.path)) fail('invalid-evidence-input');
591
+ gateArtifacts.set(gateArtifact.path, Object.freeze({ gateId: gate.id, ...gateArtifact }));
592
+ }
593
+ }
594
+
595
+ const prepared = [];
596
+ let totalBytes = 0;
597
+ for (const item of [...artifacts].sort((left, right) => left.id.localeCompare(right.id))) {
598
+ const contents = readPinnedArtifact(project, item.path);
599
+ const authoritative = gateArtifacts.get(item.path);
600
+ const coverage = value.traceability.coverage.find(entry => entry.method === 'test' && entry.evidenceId === item.testId);
601
+ if (!authoritative || !coverage || authoritative.gateId !== coverage.gateId
602
+ || authoritative.sha256 !== contents.sha256 || authoritative.bytes !== contents.size) fail('invalid-evidence-input');
603
+ totalBytes += contents.size;
604
+ if (totalBytes > MAX_TOTAL_BYTES) fail('invalid-evidence-input');
605
+ const extension = extname(item.path).toLowerCase();
606
+ if (item.type === 'screenshot') {
607
+ if (!validImage(contents.bytes, extension)) fail('invalid-evidence-input');
608
+ scanSecrets(contents.bytes);
609
+ } else {
610
+ let text;
611
+ try { text = new TextDecoder('utf-8', { fatal: true }).decode(contents.bytes); } catch { fail('invalid-evidence-input'); }
612
+ if (containsSecretMaterial(text)) fail('invalid-evidence-input');
613
+ }
614
+ prepared.push({ item, contents, destination: 'artifacts/' + item.id + (/^\.[a-z0-9]{1,10}$/.test(extension) ? extension : '.bin') });
615
+ }
616
+ for (const coverage of value.traceability.coverage) {
617
+ if (coverage.method === 'test' && !prepared.some(entry => entry.item.path === coverage.resultPath
618
+ && entry.contents.sha256 === coverage.resultSha256)) fail('invalid-evidence-input');
619
+ }
620
+
621
+ const core = immutable({
622
+ schemaVersion: 1, runId, graphId, commitSha: value.qualityRun.commitSha,
623
+ gates: value.qualityRun.gates, traceability: value.traceability, contexts,
624
+ artifacts: prepared.map(({ item, contents, destination }) => ({
625
+ id: item.id, type: item.type, testId: item.testId, sourcePath: item.path,
626
+ path: destination, sha256: contents.sha256, bytes: contents.size,
627
+ ...(item.type === 'screenshot' ? { mediaType: 'image/png' } : {}),
628
+ })),
629
+ });
630
+ const reviewContentHash = sha256(JSON.stringify(core));
631
+ const claims = [];
632
+ const reviewProofs = [];
633
+ try {
634
+ for (const review of reviewsInput) {
635
+ const receipt = invokeApproval(review.approvalFor, reviewContentHash, 'invalid-evidence-input');
636
+ const claim = claimExact(receipt, {
637
+ subjectId, action: 'quality.review',
638
+ resource: 'evidence:' + runId + ':review:' + review.id + ':sha256:' + reviewContentHash,
639
+ policyId: 'quality.review',
640
+ }, value.approvalRegistry, review.expectedApproverId, value.nowMs, false, 'invalid-evidence-input');
641
+ claims.push(claim);
642
+ reviewProofs.push(immutable({
643
+ id: review.id, reviewerId: receipt.approverId, status: 'approved', required: review.required,
644
+ approvalReceiptId: receipt.id, approverPrincipal: receipt.approverPrincipal, contentHash: reviewContentHash,
645
+ }));
646
+ }
647
+ const preapprovalContentHash = sha256(JSON.stringify({ core, reviews: reviewProofs }));
648
+ const finalReceipt = invokeApproval(finalApproval.approvalFor, preapprovalContentHash, 'approval-required');
649
+ claims.push(claimExact(finalReceipt, {
650
+ subjectId, action: 'quality.complete', resource: 'evidence:' + runId + ':sha256:' + preapprovalContentHash,
651
+ policyId: 'quality.final',
652
+ }, value.approvalRegistry, finalApproval.expectedApproverId, value.nowMs, true, 'approval-required'));
653
+
654
+ const createdAt = new Date(value.nowMs).toISOString();
655
+ const evidence = canonicalEvidenceLedger({
656
+ runId, graphId, commitSha: core.commitSha, createdAt, traceability: value.traceability,
657
+ prepared, reviews: reviewProofs, finalReceipt,
658
+ });
659
+ const manifest = immutable({
660
+ ...core, status: 'complete', createdAt, preapprovalContentHash,
661
+ reviews: reviewProofs,
662
+ finalApproval: {
663
+ approvalReceiptId: finalReceipt.id, approverId: finalReceipt.approverId,
664
+ approverPrincipal: finalReceipt.approverPrincipal, decision: 'approved', contentHash: preapprovalContentHash,
665
+ },
666
+ evidence, publication: { status: 'local-only' }, durablyPublished: false,
667
+ });
668
+ const validation = validateEvidenceManifest(manifest);
669
+ if (!validation.valid) fail('invalid-evidence-input');
670
+
671
+ const manifestText = JSON.stringify(manifest, null, 2) + '\n';
672
+ const manifestChecksum = sha256(manifestText);
673
+ const archiveFiles = [
674
+ ...prepared.map(item => ({
675
+ path: item.destination, sha256: item.contents.sha256, bytes: item.contents.size,
676
+ contentBase64: item.contents.bytes.toString('base64'),
677
+ })),
678
+ {
679
+ path: 'manifest.json', sha256: manifestChecksum, bytes: Buffer.byteLength(manifestText),
680
+ contentBase64: Buffer.from(manifestText).toString('base64'),
681
+ },
682
+ ].sort((left, right) => left.path.localeCompare(right.path));
683
+ const archiveText = JSON.stringify({ schemaVersion: 1, runId, commitSha: core.commitSha, files: archiveFiles }) + '\n';
684
+ const archiveChecksum = sha256(archiveText);
685
+
686
+ let publicationRecord = Object.freeze({ status: 'local-only' });
687
+ let publicationText = null;
688
+ if (publication) {
689
+ const receipt = invokeApproval(publication.approvalFor, archiveChecksum, 'publication-invalid');
690
+ claims.push(claimExact(receipt, {
691
+ subjectId, action: 'quality.publish', resource: publication.remoteUrl + '#sha256:' + archiveChecksum,
692
+ policyId: 'quality.publication',
693
+ }, value.approvalRegistry, publication.expectedApproverId, value.nowMs, true, 'publication-invalid'));
694
+ publicationRecord = immutable({
695
+ status: 'published', remoteUrl: publication.remoteUrl, checksum: archiveChecksum,
696
+ approvalReceiptId: receipt.id, approverId: receipt.approverId, approverPrincipal: receipt.approverPrincipal,
697
+ });
698
+ publicationText = JSON.stringify(publicationRecord, null, 2) + '\n';
699
+ }
700
+
701
+ let after;
702
+ try { after = await value.gitClient.inspectRepository(project.path); } catch { fail('invalid-evidence-input'); }
703
+ if (after.repositoryId !== repository.repositoryId || after.root !== repository.root
704
+ || after.headSha !== repository.headSha || after.dirty || after.dirtyPaths.length !== 0) fail('invalid-evidence-input');
705
+
706
+ const filesToWrite = [
707
+ ...prepared.map(item => ({ path: item.destination, bytes: item.contents.bytes })),
708
+ { path: 'manifest.json', bytes: manifestText }, { path: 'qa-bundle.json', bytes: archiveText },
709
+ ...(publicationText === null ? [] : [{ path: 'publication.json', bytes: publicationText }]),
710
+ ];
711
+ const runDirectory = publishAtomically(output, runId, filesToWrite, claims, manifest);
712
+ return immutable({
713
+ manifest, publication: publicationRecord, durablyPublished: publication !== null,
714
+ runDirectory, manifestPath: join(runDirectory, 'manifest.json'), manifestChecksum,
715
+ archivePath: join(runDirectory, 'qa-bundle.json'), archiveChecksum,
716
+ files: [
717
+ ...archiveFiles.map(item => ({ path: item.path, sha256: item.sha256, bytes: item.bytes })),
718
+ { path: 'qa-bundle.json', sha256: archiveChecksum, bytes: Buffer.byteLength(archiveText) },
719
+ ...(publicationText === null ? [] : [{ path: 'publication.json', sha256: sha256(publicationText), bytes: Buffer.byteLength(publicationText) }]),
720
+ ],
721
+ });
722
+ } catch (error) {
723
+ for (const claim of claims) claim.release();
724
+ if (error instanceof EvidenceCollectionError) throw error;
725
+ fail('invalid-evidence-input');
726
+ }
727
+ }