@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,215 @@
1
+ import { claimApproval } from '../policy/approvals.js';
2
+ import { assertQualityRun } from './runner.js';
3
+
4
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
5
+ const AC_ID = /^[A-Z][A-Z0-9]+-[1-9][0-9]*-AC[1-9][0-9]*$/;
6
+ const traceabilityResults = new WeakSet();
7
+
8
+ export class TraceabilityError extends Error {
9
+ constructor(reason = 'invalid-traceability-input') {
10
+ super('Acceptance traceability input is invalid.');
11
+ this.name = 'TraceabilityError';
12
+ this.code = 'ERR_TRACEABILITY';
13
+ this.safeMessage = this.message;
14
+ this.details = Object.freeze({ reason });
15
+ }
16
+ }
17
+
18
+ function fail(reason) { throw new TraceabilityError(reason); }
19
+
20
+ function capture(value, allowed, required = []) {
21
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('invalid-traceability-input');
22
+ let keys;
23
+ try { keys = Reflect.ownKeys(value); } catch { fail('invalid-traceability-input'); }
24
+ if (keys.some(key => typeof key !== 'string' || !allowed.has(key))) fail('invalid-traceability-input');
25
+ const result = Object.create(null);
26
+ for (const key of keys) {
27
+ let descriptor;
28
+ try { descriptor = Object.getOwnPropertyDescriptor(value, key); } catch { fail('invalid-traceability-input'); }
29
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-traceability-input');
30
+ result[key] = descriptor.value;
31
+ }
32
+ if (required.some(key => !Object.hasOwn(result, key))) fail('invalid-traceability-input');
33
+ return result;
34
+ }
35
+
36
+ function array(value, maximum, convert) {
37
+ try {
38
+ if (!Array.isArray(value)) fail('invalid-traceability-input');
39
+ const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
40
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximum
41
+ || Reflect.ownKeys(value).length !== length + 1) fail('invalid-traceability-input');
42
+ const result = [];
43
+ for (let index = 0; index < length; index += 1) {
44
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
45
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) fail('invalid-traceability-input');
46
+ result.push(convert(descriptor.value));
47
+ }
48
+ return result;
49
+ } catch (error) {
50
+ if (error instanceof TraceabilityError) throw error;
51
+ fail('invalid-traceability-input');
52
+ }
53
+ }
54
+
55
+ function id(value) {
56
+ if (typeof value !== 'string' || value.length > 64 || !ID.test(value)) fail('invalid-traceability-input');
57
+ return value;
58
+ }
59
+
60
+ function acId(value) {
61
+ if (typeof value !== 'string' || value.length > 100 || !AC_ID.test(value)) fail('invalid-traceability-input');
62
+ return value;
63
+ }
64
+
65
+ function criterion(value) {
66
+ const input = capture(value, new Set(['id', 'inScope']), ['id', 'inScope']);
67
+ if (typeof input.inScope !== 'boolean') fail('invalid-traceability-input');
68
+ return Object.freeze({ id: acId(input.id), inScope: input.inScope });
69
+ }
70
+
71
+ function manualEvidence(value) {
72
+ const input = capture(value, new Set([
73
+ 'id', 'acceptanceCriterion', 'expectedApproverId', 'approval',
74
+ ]), ['id', 'acceptanceCriterion', 'expectedApproverId', 'approval']);
75
+ return Object.freeze({
76
+ id: id(input.id),
77
+ acceptanceCriterion: acId(input.acceptanceCriterion),
78
+ expectedApproverId: id(input.expectedApproverId),
79
+ approval: input.approval,
80
+ });
81
+ }
82
+
83
+ export function assertTraceabilityResult(value) {
84
+ if (!traceabilityResults.has(value)) fail('invalid-traceability-input');
85
+ }
86
+
87
+ function immutable(value) {
88
+ if (Array.isArray(value)) return Object.freeze(value.map(immutable));
89
+ if (value && typeof value === 'object') {
90
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, immutable(child)])));
91
+ }
92
+ return value;
93
+ }
94
+
95
+ export function validateTraceability(input, options = {}) {
96
+ const value = capture(input, new Set([
97
+ 'subjectId', 'qualityRun', 'acceptanceCriteria', 'manualReviews',
98
+ ]), ['subjectId', 'qualityRun', 'acceptanceCriteria', 'manualReviews']);
99
+ const configured = capture(options, new Set(['approvalRegistry', 'nowMs']), ['approvalRegistry', 'nowMs']);
100
+ const subjectId = id(value.subjectId);
101
+ try { assertQualityRun(value.qualityRun); } catch { fail('invalid-traceability-input'); }
102
+ if (!Number.isSafeInteger(configured.nowMs) || configured.nowMs < 0) fail('invalid-traceability-input');
103
+ const criteria = array(value.acceptanceCriteria, 2_000, criterion);
104
+ const tests = value.qualityRun.tests;
105
+ const manualReviews = array(value.manualReviews, 2_000, manualEvidence);
106
+ if (criteria.length === 0
107
+ || new Set(criteria.map(item => item.id.toLowerCase())).size !== criteria.length
108
+ || new Set(tests.map(item => item.id.toLowerCase())).size !== tests.length
109
+ || new Set(manualReviews.map(item => item.id.toLowerCase())).size !== manualReviews.length
110
+ || new Set(manualReviews.map(item => item.acceptanceCriterion.toLowerCase())).size !== manualReviews.length) {
111
+ fail('invalid-traceability-input');
112
+ }
113
+ const known = new Set(criteria.map(item => item.id));
114
+ if (tests.some(item => item.acceptanceCriteria.some(ac => !known.has(ac)))
115
+ || manualReviews.some(item => !known.has(item.acceptanceCriterion))) fail('unknown-acceptance-criterion');
116
+
117
+ const manualRequired = new Set(criteria
118
+ .filter(item => item.inScope && !tests.some(test => test.status === 'passed' && test.acceptanceCriteria.includes(item.id)))
119
+ .map(item => item.id));
120
+ if (manualReviews.some(review => !manualRequired.has(review.acceptanceCriterion))) {
121
+ fail('invalid-traceability-input');
122
+ }
123
+
124
+ const approvedManual = new Map();
125
+ const approvalErrors = new Map();
126
+ const claims = [];
127
+ for (const review of manualReviews) {
128
+ let claim = { valid: false };
129
+ try {
130
+ claim = claimApproval(review.approval, {
131
+ subjectId,
132
+ action: 'quality.manual-review',
133
+ resource: 'acceptance:' + review.acceptanceCriterion + ':manual-review:' + review.id,
134
+ policyId: 'quality.acceptance',
135
+ }, {
136
+ registry: configured.approvalRegistry,
137
+ expectedApproverId: review.expectedApproverId,
138
+ requireHumanApprover: true,
139
+ requireSingleUse: true,
140
+ nowMs: configured.nowMs,
141
+ });
142
+ } catch { claim = { valid: false }; }
143
+ if (claim.valid) {
144
+ claims.push({ claim, review });
145
+ approvedManual.set(review.acceptanceCriterion, review);
146
+ }
147
+ else approvalErrors.set(review.acceptanceCriterion, 'Acceptance criterion ' + review.acceptanceCriterion + ' lacks exact human approval.');
148
+ }
149
+
150
+ const coverage = [];
151
+ const errors = [];
152
+ const outOfScope = [];
153
+ for (const criterionEntry of [...criteria].sort((left, right) => left.id.localeCompare(right.id))) {
154
+ if (!criterionEntry.inScope) {
155
+ outOfScope.push(criterionEntry.id);
156
+ continue;
157
+ }
158
+ const passed = tests
159
+ .filter(item => item.status === 'passed' && item.acceptanceCriteria.includes(criterionEntry.id))
160
+ .sort((left, right) => left.id.localeCompare(right.id))[0];
161
+ if (passed) {
162
+ coverage.push({
163
+ acceptanceCriterion: criterionEntry.id,
164
+ method: 'test',
165
+ evidenceId: passed.id,
166
+ gateId: passed.gateId,
167
+ resultPath: passed.resultPath,
168
+ resultSha256: passed.resultSha256,
169
+ });
170
+ continue;
171
+ }
172
+ const manual = approvedManual.get(criterionEntry.id);
173
+ if (manual) {
174
+ coverage.push({
175
+ acceptanceCriterion: criterionEntry.id,
176
+ method: 'manual-review',
177
+ evidenceId: manual.id,
178
+ approvalReceiptId: manual.approval.id,
179
+ approverId: manual.approval.approverId,
180
+ approverPrincipal: manual.approval.approverPrincipal,
181
+ });
182
+ continue;
183
+ }
184
+ errors.push(approvalErrors.get(criterionEntry.id)
185
+ ?? 'Acceptance criterion ' + criterionEntry.id + ' lacks a passed deterministic test or exact human approval.');
186
+ }
187
+ if (errors.length > 0) {
188
+ for (const entry of claims) entry.claim.release();
189
+ } else {
190
+ const finalized = [];
191
+ try {
192
+ for (const entry of claims) {
193
+ if (!entry.claim.finalize()) fail('approval-transaction-failed');
194
+ finalized.push(entry);
195
+ }
196
+ for (const entry of claims) {
197
+ if (!entry.claim.publish()) fail('approval-transaction-failed');
198
+ }
199
+ } catch (error) {
200
+ for (const entry of finalized) entry.claim.rollback();
201
+ for (const entry of claims) if (!finalized.includes(entry)) entry.claim.release();
202
+ if (error instanceof TraceabilityError) throw error;
203
+ fail('approval-transaction-failed');
204
+ }
205
+ }
206
+ const output = immutable({
207
+ valid: errors.length === 0,
208
+ commitSha: value.qualityRun.commitSha,
209
+ coverage,
210
+ outOfScope,
211
+ errors,
212
+ });
213
+ traceabilityResults.add(output);
214
+ return output;
215
+ }
@@ -0,0 +1,59 @@
1
+ import { resolve } from 'node:path';
2
+ import { boundedString, captureRecord, failProvider } from '../adapters/contract.js';
3
+ import { runArgv } from '../discovery/tools.js';
4
+
5
+ const HOSTS = Object.freeze({ 'github.com': 'github', 'bitbucket.org': 'bitbucket', 'gitlab.com': 'gitlab' });
6
+ const SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,99}$/;
7
+
8
+ export function parseRepositoryRemote(input) {
9
+ const value = boundedString(input, 2048);
10
+ // Parse before URL normalization, which would otherwise silently remove traversal.
11
+ const match = /^(?:https:\/\/|ssh:\/\/git@)([^/:?#]+)\/([^?#]+)$/.exec(value)
12
+ ?? /^git@([^/:?#]+):([^?#]+)$/.exec(value);
13
+ if (!match || !Object.hasOwn(HOSTS, match[1])) failProvider('invalid-request');
14
+ const host = match[1];
15
+ const fullName = match[2].replace(/\.git$/, '');
16
+ const segments = fullName.split('/');
17
+ if (segments.length < 2 || segments.length > 20 || fullName.length > 255
18
+ || (host !== 'gitlab.com' && segments.length !== 2)
19
+ || segments.some(part => !SEGMENT.test(part) || part === '.' || part === '..' || part.endsWith('.'))) failProvider('invalid-request');
20
+ return Object.freeze({ provider: HOSTS[host], host, namespace: segments.slice(0, -1).join('/'), name: segments.at(-1), fullName, url: `https://${host}/${fullName}` });
21
+ }
22
+
23
+ export function selectRepositoryRemote(input, options = {}) {
24
+ const { remoteName } = captureRecord(options, new Set(['remoteName']), []);
25
+ if (remoteName !== undefined) boundedString(remoteName, 100, /^[A-Za-z0-9][A-Za-z0-9._-]*$/);
26
+ if (!Array.isArray(input) || input.length < 1 || input.length > 100) failProvider('invalid-request');
27
+ const candidates = input.map(value => {
28
+ const remote = captureRecord(value, new Set(['name', 'url']), ['name', 'url']);
29
+ boundedString(remote.name, 100, /^[A-Za-z0-9][A-Za-z0-9._-]*$/);
30
+ return remote;
31
+ }).filter(remote => remoteName === undefined || remote.name === remoteName);
32
+ if (candidates.length === 0) failProvider('invalid-request');
33
+ const parsed = candidates.map(remote => ({ ...parseRepositoryRemote(remote.url), remoteName: remote.name }));
34
+ if (new Set(parsed.map(remote => remote.url)).size !== 1) failProvider('invalid-request');
35
+ return Object.freeze(parsed[0]);
36
+ }
37
+
38
+ export async function discoverRepositoryRemotes(projectRoot, { runner = runArgv } = {}) {
39
+ let result;
40
+ try { result = await runner('git', ['remote', '-v'], { cwd: resolve(projectRoot), shell: false, timeoutMs: 3000, maxOutputBytes: 32768 }); }
41
+ catch { failProvider('invalid-request'); }
42
+ if (result?.code !== 0 || result.timedOut === true || result.truncated?.stdout === true || typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > 32768) failProvider('invalid-request');
43
+ const remotes = [];
44
+ for (const line of result.stdout.trim().split('\n').filter(Boolean)) {
45
+ const match = /^(\S+)\s+(\S+) \((fetch|push)\)$/.exec(line);
46
+ if (!match) failProvider('invalid-request');
47
+ if (match[3] === 'fetch') remotes.push(Object.freeze({ name: boundedString(match[1], 100, /^[A-Za-z0-9][A-Za-z0-9._-]*$/), url: boundedString(match[2], 2048) }));
48
+ }
49
+ if (remotes.length > 100) failProvider('invalid-request');
50
+ return Object.freeze(remotes);
51
+ }
52
+
53
+ // Explicit per-operation choices do not rewrite the tracked onboarding preference.
54
+ export function selectConfiguredRepositoryRemote(remotes, preference, remoteName) {
55
+ const selected = selectRepositoryRemote(remotes, remoteName !== undefined ? {remoteName}
56
+ : preference ? {remoteName: preference.name} : {});
57
+ if (remoteName === undefined && preference && selected.url !== preference.url) failProvider('invalid-request');
58
+ return selected;
59
+ }
@@ -0,0 +1,2 @@
1
+ export { parseRepositoryRemote, selectRepositoryRemote, selectConfiguredRepositoryRemote, discoverRepositoryRemotes } from './identity.js';
2
+ export { createRepositoryProvider } from './provider.js';
@@ -0,0 +1,210 @@
1
+ import { boundedString, captureRecord, failProvider, immutableRedactedJson, providerTimestamp } from '../adapters/contract.js';
2
+ import { createGithubAdapter } from '../adapters/github.js';
3
+ import { createProviderHttpClient } from '../adapters/http.js';
4
+ import { parseRepositoryRemote } from './identity.js';
5
+
6
+ const ENDPOINTS = Object.freeze({ github: 'https://api.github.com', bitbucket: 'https://api.bitbucket.org/2.0', gitlab: 'https://gitlab.com/api/v4' });
7
+ const CAPABILITIES = Object.freeze({ read: Object.freeze(['repository', 'branch', 'review-request', 'checks', 'reviews']), write: Object.freeze([]) });
8
+ const SHA = /^[a-f0-9]{40}$/;
9
+ const BRANCH = /^[A-Za-z0-9_][A-Za-z0-9._/-]{0,254}$/;
10
+ function remote(condition, provider) { if (!condition) failProvider('remote', { provider, retryClassification: 'permanent' }); }
11
+ function record(value, provider) { remote(value !== null && typeof value === 'object' && !Array.isArray(value), provider); return value; }
12
+ function text(value, provider, maximum = 1024) { remote(typeof value === 'string' && value.length > 0 && value.length <= maximum, provider); return value; }
13
+ function sha(value, provider) { remote(typeof value === 'string' && SHA.test(value), provider); return value; }
14
+ function ref(value, provider) { remote(typeof value === 'string' && BRANCH.test(value) && !value.includes('..') && !value.includes('//'), provider); return value; }
15
+ function positive(value, provider) { remote(Number.isSafeInteger(value) && value > 0, provider); return value; }
16
+ function boundUrl(value, expected, provider) { remote(value === expected, provider); return value; }
17
+ function number(value) { if (!Number.isSafeInteger(value) || value < 1 || value > 1_000_000_000) failProvider('invalid-request'); return value; }
18
+
19
+ export function createRepositoryProvider(input) {
20
+ const config = captureRecord(input, new Set(['repository', 'transport', 'headers', 'timeoutMs', 'maxResponseBytes', 'maxPages', 'maxItems', 'clock']), ['repository', 'transport'], 'invalid-config');
21
+ const supplied = captureRecord(config.repository, new Set(['provider', 'host', 'namespace', 'name', 'fullName', 'url', 'remoteName']), ['provider', 'host', 'namespace', 'name', 'fullName', 'url'], 'invalid-config');
22
+ const repository = parseRepositoryRemote(supplied.url);
23
+ for (const key of Object.keys(repository)) if (repository[key] !== supplied[key]) failProvider('invalid-config');
24
+ const provider = repository.provider;
25
+ const clock = config.clock ?? (() => new Date().toISOString());
26
+ if (typeof clock !== 'function') failProvider('invalid-config', { provider });
27
+ const limits = Object.fromEntries(['headers', 'timeoutMs', 'maxResponseBytes', 'maxPages', 'maxItems'].filter(key => config[key] !== undefined).map(key => [key, config[key]]));
28
+ const http = createProviderHttpClient({ provider, transport: config.transport, baseUrl: ENDPOINTS[provider], ...limits, allowEncodedSlash: true });
29
+ const github = provider === 'github' ? createGithubAdapter({ transport: config.transport, baseUrl: ENDPOINTS.github, ...limits, clock }) : null;
30
+ const root = provider === 'github' ? `/repos/${repository.fullName}` : provider === 'bitbucket' ? `/repositories/${repository.fullName}` : `/projects/${encodeURIComponent(repository.fullName)}`;
31
+ const reviewRoot = provider === 'github' ? 'pulls' : provider === 'bitbucket' ? 'pullrequests' : 'merge_requests';
32
+ const maxPages = config.maxPages ?? 20, maxItems = config.maxItems ?? 10_000;
33
+ const request = (path, signal) => http.request({ method: 'GET', path, ...(signal === undefined ? {} : { signal }) });
34
+
35
+ async function bitbucketPages(path, signal) {
36
+ const initial = new URL(http.urlFor(`${path}?pagelen=100`));
37
+ const output = [], seen = new Set();
38
+ let next = initial.href, declaredSize;
39
+ for (let page = 0; page < maxPages; page += 1) {
40
+ let url;
41
+ try { url = new URL(next); } catch { failProvider('remote', { provider }); }
42
+ remote(url.origin === initial.origin && url.pathname === initial.pathname && !url.username && !url.password && !url.hash, provider);
43
+ remote([...url.searchParams.keys()].every(key => ['page', 'pagelen'].includes(key)) && url.searchParams.getAll('pagelen').length === 1
44
+ && url.searchParams.get('pagelen') === '100' && url.searchParams.getAll('page').length <= 1
45
+ && (!url.searchParams.has('page') || /^[1-9][0-9]{0,8}$/.test(url.searchParams.get('page'))), provider);
46
+ remote(Number(url.searchParams.get('page') ?? 1) === page + 1, provider);
47
+ remote(!seen.has(url.href), provider); seen.add(url.href);
48
+ const response = await request(`${url.pathname.slice('/2.0'.length)}${url.search}`, signal);
49
+ remote(Array.isArray(response.data?.values), provider);
50
+ if (response.data.size !== undefined) {
51
+ remote(Number.isSafeInteger(response.data.size) && response.data.size >= 0 && (declaredSize === undefined || declaredSize === response.data.size), provider);
52
+ declaredSize = response.data.size;
53
+ if (declaredSize > maxItems) failProvider('pagination-limit', { provider });
54
+ }
55
+ if (response.data.page !== undefined) remote(response.data.page === page + 1, provider);
56
+ output.push(...response.data.values);
57
+ if (declaredSize !== undefined) remote(output.length <= declaredSize, provider);
58
+ if (output.length > maxItems) failProvider('pagination-limit', { provider });
59
+ if (response.data.next === undefined || response.data.next === null) {
60
+ if (declaredSize !== undefined) remote(output.length === declaredSize, provider);
61
+ return output;
62
+ }
63
+ remote(typeof response.data.next === 'string' && response.data.next.length <= 4096 && response.data.values.length > 0, provider);
64
+ next = response.data.next;
65
+ }
66
+ failProvider('pagination-limit', { provider });
67
+ }
68
+
69
+ async function repositoryInfo(signal) {
70
+ if (github) {
71
+ const value = (await github.read({ kind: 'repo', owner: repository.namespace, repo: repository.name, ...(signal === undefined ? {} : { signal }) })).normalized.repository;
72
+ return { ...repository, id: value.id, defaultBranch: value.defaultBranch, private: value.private };
73
+ }
74
+ const value = (await request(root, signal)).data;
75
+ if (provider === 'bitbucket') {
76
+ remote(value?.full_name === repository.fullName && typeof value.is_private === 'boolean', provider);
77
+ boundUrl(value.links?.html?.href, repository.url, provider);
78
+ return { ...repository, id: text(value.uuid, provider), defaultBranch: value.mainbranch === null ? null : ref(value.mainbranch?.name, provider), private: value.is_private };
79
+ }
80
+ remote(value?.path_with_namespace === repository.fullName && ['private', 'internal', 'public'].includes(value.visibility), provider);
81
+ boundUrl(value.web_url, repository.url, provider);
82
+ return { ...repository, id: positive(value.id, provider), defaultBranch: value.default_branch === null ? null : ref(value.default_branch, provider), private: value.visibility !== 'public' };
83
+ }
84
+
85
+ async function branchInfo(branch, signal) {
86
+ boundedString(branch, 255, BRANCH); if (branch.includes('..') || branch.includes('//')) failProvider('invalid-request');
87
+ if (github) {
88
+ const value = (await request(`${root}/branches/${encodeURIComponent(branch)}`, signal)).data;
89
+ remote(value?.name === branch && typeof value.protected === 'boolean', provider);
90
+ return { name: branch, commitSha: sha(value.commit?.sha, provider), protected: value.protected };
91
+ }
92
+ const value = (await request(`${root}/${provider === 'bitbucket' ? 'refs' : 'repository'}/branches/${encodeURIComponent(branch)}`, signal)).data;
93
+ remote(value?.name === branch, provider);
94
+ return { name: branch, commitSha: sha(provider === 'bitbucket' ? value.target?.hash : value.commit?.id, provider), protected: provider === 'bitbucket' ? null : typeof value.protected === 'boolean' ? value.protected : null };
95
+ }
96
+
97
+ async function reviewInfo(reviewNumber, signal) {
98
+ const value = (await request(`${root}/${reviewRoot}/${number(reviewNumber)}`, signal)).data;
99
+ let result;
100
+ if (provider === 'github') {
101
+ remote(value?.number === reviewNumber && ['open', 'closed'].includes(value.state) && typeof value.merged === 'boolean', provider);
102
+ result = { number: reviewNumber, state: value.merged ? 'merged' : value.state, title: text(value.title, provider), headSha: sha(value.head?.sha, provider), headBranch: ref(value.head?.ref, provider), baseBranch: ref(value.base?.ref, provider), url: boundUrl(value.html_url, `${repository.url}/pull/${reviewNumber}`, provider) };
103
+ } else if (provider === 'bitbucket') {
104
+ remote(value?.id === reviewNumber && ['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'].includes(value.state) && value.destination?.repository?.full_name === repository.fullName, provider);
105
+ result = { number: reviewNumber, state: value.state === 'OPEN' ? 'open' : value.state === 'MERGED' ? 'merged' : 'closed', title: text(value.title, provider), headSha: sha(value.source?.commit?.hash, provider), headBranch: ref(value.source?.branch?.name, provider), baseBranch: ref(value.destination?.branch?.name, provider), url: boundUrl(value.links?.html?.href, `${repository.url}/pull-requests/${reviewNumber}`, provider) };
106
+ } else {
107
+ remote(value?.iid === reviewNumber && ['opened', 'closed', 'locked', 'merged'].includes(value.state), provider);
108
+ result = { number: reviewNumber, state: value.state === 'opened' ? 'open' : value.state, title: text(value.title, provider), headSha: sha(value.sha, provider), headBranch: ref(value.source_branch, provider), baseBranch: ref(value.target_branch, provider), url: boundUrl(value.web_url, `${repository.url}/-/merge_requests/${reviewNumber}`, provider) };
109
+ }
110
+ return { result, raw: value };
111
+ }
112
+
113
+ async function checksInfo(commitSha, signal) {
114
+ boundedString(commitSha, 40, SHA);
115
+ const options = signal === undefined ? {} : { signal };
116
+ let items;
117
+ if (provider === 'github') {
118
+ const checks = await http.paginate({ path: `${root}/commits/${commitSha}/check-runs?per_page=100`, itemsKey: 'check_runs', identityKey: 'id', totalKey: 'total_count', ...options });
119
+ const statuses = await http.paginate({ path: `${root}/commits/${commitSha}/statuses?per_page=100`, identityKey: 'id', ...options });
120
+ items = checks.map(value => {
121
+ record(value, provider);
122
+ positive(value.id, provider); sha(value.head_sha, provider);
123
+ if (value.head_sha !== commitSha) failProvider('state-conflict', { provider });
124
+ remote(['queued', 'in_progress', 'completed', 'pending', 'requested', 'waiting'].includes(value.status), provider);
125
+ const conclusions = { success: 'success', failure: 'failure', timed_out: 'failure', cancelled: 'cancelled', skipped: 'skipped', neutral: 'neutral', action_required: 'blocked', stale: 'stale' };
126
+ if (value.status === 'completed') remote(Object.hasOwn(conclusions, value.conclusion), provider);
127
+ return { id: `check:${value.id}`, name: text(value.name, provider), commitSha, state: value.status === 'completed' ? conclusions[value.conclusion] : 'pending' };
128
+ });
129
+ items.push(...statuses.map(value => {
130
+ record(value, provider);
131
+ positive(value.id, provider);
132
+ if (value.sha !== undefined && value.sha !== commitSha) failProvider('state-conflict', { provider });
133
+ remote(['success', 'failure', 'error', 'pending'].includes(value.state), provider);
134
+ return { id: `status:${value.id}`, name: text(value.context, provider), commitSha, state: value.state === 'error' ? 'failure' : value.state };
135
+ }));
136
+ } else if (provider === 'bitbucket') {
137
+ const values = await bitbucketPages(`${root}/commit/${commitSha}/statuses`, signal);
138
+ items = values.map(value => {
139
+ record(value, provider);
140
+ if (value.links?.commit?.href !== undefined && value.links.commit.href !== `${ENDPOINTS.bitbucket}${root}/commit/${commitSha}`) failProvider('state-conflict', { provider });
141
+ const states = { SUCCESSFUL: 'success', FAILED: 'failure', INPROGRESS: 'pending', STOPPED: 'cancelled' };
142
+ remote(Object.hasOwn(states, value.state), provider);
143
+ return { id: text(value.key, provider), name: text(value.name || value.key, provider), commitSha, state: states[value.state] };
144
+ });
145
+ } else {
146
+ const values = await http.paginate({ path: `${root}/repository/commits/${commitSha}/statuses?per_page=100`, identityKey: 'id', ...options });
147
+ items = values.map(value => {
148
+ record(value, provider);
149
+ positive(value.id, provider); sha(value.sha, provider);
150
+ if (value.sha !== commitSha) failProvider('state-conflict', { provider });
151
+ const states = { pending: 'pending', running: 'pending', success: 'success', failed: 'failure', canceled: 'cancelled', skipped: 'skipped', created: 'pending', manual: 'blocked', scheduled: 'pending', preparing: 'pending', waiting_for_resource: 'pending' };
152
+ remote(Object.hasOwn(states, value.status), provider);
153
+ return { id: String(value.id), name: text(value.name, provider), commitSha, state: states[value.status] };
154
+ });
155
+ }
156
+ remote(new Set(items.map(value => value.id)).size === items.length, provider);
157
+ return { commitSha, items, requiredPolicy: 'unknown' };
158
+ }
159
+
160
+ async function reviewsInfo(reviewNumber, signal) {
161
+ number(reviewNumber);
162
+ let items;
163
+ if (provider === 'github') {
164
+ const values = await http.paginate({ path: `${root}/pulls/${reviewNumber}/reviews?per_page=100`, identityKey: 'id', ...(signal === undefined ? {} : {signal}) });
165
+ items = values.map(value => {
166
+ record(value, provider);
167
+ positive(value.id, provider); remote(['PENDING', 'COMMENTED', 'APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(value.state), provider);
168
+ return { id: String(value.id), reviewer: text(value.user?.login, provider), state: value.state.toLowerCase(), commitSha: value.commit_id == null ? null : sha(value.commit_id, provider) };
169
+ });
170
+ } else if (provider === 'bitbucket') {
171
+ const { raw } = await reviewInfo(reviewNumber, signal);
172
+ remote(Array.isArray(raw.participants) && raw.participants.length <= maxItems, provider);
173
+ items = raw.participants.map(value => {
174
+ record(value, provider);
175
+ remote(typeof value.approved === 'boolean', provider);
176
+ return { reviewer: text(value.user?.uuid, provider), state: value.approved ? 'approved' : value.state === 'changes_requested' ? 'changes_requested' : 'unapproved', commitSha: null };
177
+ });
178
+ } else {
179
+ const value = (await request(`${root}/merge_requests/${reviewNumber}/approvals`, signal)).data;
180
+ remote(value?.iid === reviewNumber && Array.isArray(value.approved_by) && value.approved_by.length <= maxItems, provider);
181
+ items = value.approved_by.map(entry => ({ reviewer: text(record(entry, provider).user?.username, provider), state: 'approved', commitSha: null }));
182
+ }
183
+ return { items, policy: 'unknown' };
184
+ }
185
+
186
+ async function read(inputRead) {
187
+ const value = captureRecord(inputRead, new Set(['kind', 'branch', 'number', 'commitSha', 'signal']), ['kind']);
188
+ if (!CAPABILITIES.read.includes(value.kind)) failProvider('invalid-request', { provider });
189
+ let normalized;
190
+ if (value.kind === 'repository') normalized = { repository: await repositoryInfo(value.signal) };
191
+ else if (value.kind === 'branch') normalized = { branch: await branchInfo(value.branch, value.signal) };
192
+ else if (value.kind === 'review-request') normalized = { reviewRequest: (await reviewInfo(value.number, value.signal)).result };
193
+ else if (value.kind === 'checks') normalized = { checks: await checksInfo(value.commitSha, value.signal) };
194
+ else normalized = { reviews: await reviewsInfo(value.number, value.signal) };
195
+ return immutableRedactedJson({ repository, ...normalized, observedAt: providerTimestamp(clock, provider), capabilities: CAPABILITIES });
196
+ }
197
+
198
+ async function inspect(inputInspect) {
199
+ const value = captureRecord(inputInspect, new Set(['number', 'signal']), ['number']);
200
+ number(value.number);
201
+ const info = await repositoryInfo(value.signal);
202
+ const review = (await reviewInfo(value.number, value.signal)).result;
203
+ const checks = await checksInfo(review.headSha, value.signal);
204
+ const reviews = await reviewsInfo(value.number, value.signal);
205
+ const latest = (await reviewInfo(value.number, value.signal)).result;
206
+ if (latest.headSha !== review.headSha || latest.state !== review.state || latest.baseBranch !== review.baseBranch) failProvider('state-conflict', { provider });
207
+ return immutableRedactedJson({ repository: info, reviewRequest: latest, checks, reviews, observedAt: providerTimestamp(clock, provider), consistency: 'head-rechecked', capabilities: CAPABILITIES });
208
+ }
209
+ return Object.freeze({ provider, repository, capabilities: CAPABILITIES, read, inspect, write() { failProvider('read-only', { provider }); } });
210
+ }