@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.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { captureRecord, createProviderWireBody } from '../adapters/contract.js';
|
|
2
|
+
import { createProviderHttpClient } from '../adapters/http.js';
|
|
3
|
+
import { parseRepositoryRemote } from '../repositories/identity.js';
|
|
4
|
+
import { createTrustedDeliveryExecutor } from './service.js';
|
|
5
|
+
import { digest, ensure, exact, factsDigest, hash, plain, sha, timestamp, validateCandidate } from './contract.js';
|
|
6
|
+
|
|
7
|
+
export function reviewRequestPayload(candidate) {
|
|
8
|
+
validateCandidate(candidate);
|
|
9
|
+
return plain({
|
|
10
|
+
title: `Rivet: ${candidate.runId}`,
|
|
11
|
+
body: ['Rivet verified delivery candidate', '', `Run: ${candidate.runId}`,
|
|
12
|
+
`Source branch: ${candidate.sourceBranch}`, `Target branch: ${candidate.targetBranch}`,
|
|
13
|
+
`Verified commit: ${candidate.headSha}`, `Verification evidence: ${candidate.localVerification.evidenceDigest}`,
|
|
14
|
+
'', 'Review the changes and repository checks before making a separate merge decision.'].join('\n'),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function reviewRequestContent(operation) {
|
|
19
|
+
ensure(operation.action === 'review-request');
|
|
20
|
+
digest(operation.digest);
|
|
21
|
+
const payload = plain(operation.payload);
|
|
22
|
+
exact(payload, ['title', 'body']);
|
|
23
|
+
ensure(typeof payload.title === 'string' && payload.title.trim() === payload.title && payload.title.length >= 1
|
|
24
|
+
&& payload.title.length <= 256 && !/[\u0000-\u001f\u007f]/.test(payload.title));
|
|
25
|
+
ensure(typeof payload.body === 'string' && payload.body.length >= 1 && payload.body.length <= 32000
|
|
26
|
+
&& !/[\u0000-\u0008\u000b-\u001f\u007f]/.test(payload.body)
|
|
27
|
+
&& !payload.body.includes('rivet-review-operation:'));
|
|
28
|
+
return plain({ title: payload.title, body: `${payload.body}\n\n<!-- rivet-review-operation:${operation.digest} -->` });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// These APIs accept branch names, not an atomic expected-head condition. The
|
|
32
|
+
// executor advertises that limitation and verifies the created review afterward.
|
|
33
|
+
export function createReviewRequestExecutor(kind, inputConfig) {
|
|
34
|
+
ensure(['github', 'gitlab'].includes(kind));
|
|
35
|
+
const input = captureRecord(inputConfig, new Set(['repository', 'transport', 'headers', 'clock', 'timeoutMs']), ['repository', 'transport'], 'invalid-config');
|
|
36
|
+
const supplied = plain(input.repository), repository = parseRepositoryRemote(supplied.url);
|
|
37
|
+
ensure(repository.provider === kind && hash(repository) === hash(supplied));
|
|
38
|
+
const github = kind === 'github';
|
|
39
|
+
const clock = input.clock ?? (() => new Date().toISOString());
|
|
40
|
+
ensure(typeof clock === 'function');
|
|
41
|
+
const http = createProviderHttpClient({ provider: kind, baseUrl: github ? 'https://api.github.com' : 'https://gitlab.com/api/v4',
|
|
42
|
+
transport: input.transport, ...(input.headers === undefined ? {} : { headers: input.headers }),
|
|
43
|
+
...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }), allowEncodedSlash: true });
|
|
44
|
+
const root = github ? `/repos/${repository.fullName}` : `/projects/${encodeURIComponent(repository.fullName)}`;
|
|
45
|
+
const reviewsPath = github ? '/pulls' : '/merge_requests';
|
|
46
|
+
const get = async path => (await http.request({ method: 'GET', path: root + path })).data;
|
|
47
|
+
function number(value) { ensure(Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000, 'changed-facts'); return value; }
|
|
48
|
+
function repositoryId(value) { ensure(Number.isSafeInteger(value) && value > 0, 'changed-facts'); return value; }
|
|
49
|
+
function target(value) { validateCandidate(value); ensure(hash(value.repository) === hash(repository), 'provider-mismatch'); }
|
|
50
|
+
function identity(value) {
|
|
51
|
+
ensure((github ? value.full_name : value.path_with_namespace) === repository.fullName
|
|
52
|
+
&& (github ? value.html_url : value.web_url) === repository.url && value.archived === false, 'changed-facts');
|
|
53
|
+
return { id: repositoryId(value.id), fullName: repository.fullName, url: repository.url };
|
|
54
|
+
}
|
|
55
|
+
function review(value, candidate, project) {
|
|
56
|
+
const n = number(github ? value.number : value.iid);
|
|
57
|
+
ensure((github ? ['open', 'closed'] : ['opened', 'closed', 'merged']).includes(value.state)
|
|
58
|
+
&& typeof value.draft === 'boolean', 'changed-facts');
|
|
59
|
+
const url = github ? value.html_url : value.web_url;
|
|
60
|
+
ensure(url === `${repository.url}/${github ? 'pull' : '-/merge_requests'}/${n}`, 'changed-facts');
|
|
61
|
+
if (github) {
|
|
62
|
+
ensure(typeof value.merged === 'boolean'
|
|
63
|
+
&& value.head?.repo?.id === project.id && value.base?.repo?.id === project.id
|
|
64
|
+
&& value.head?.repo?.full_name === repository.fullName && value.base?.repo?.full_name === repository.fullName
|
|
65
|
+
&& value.head?.ref === candidate.sourceBranch && value.base?.ref === candidate.targetBranch
|
|
66
|
+
&& value.head?.sha === candidate.headSha, 'changed-facts');
|
|
67
|
+
sha(value.base.sha);
|
|
68
|
+
} else ensure(value.project_id === project.id && value.source_project_id === project.id && value.target_project_id === project.id
|
|
69
|
+
&& value.source_branch === candidate.sourceBranch && value.target_branch === candidate.targetBranch
|
|
70
|
+
&& value.sha === candidate.headSha, 'changed-facts');
|
|
71
|
+
ensure(typeof value.title === 'string' && typeof (github ? value.body : value.description) === 'string', 'changed-facts');
|
|
72
|
+
return plain({ number: n, state: github ? (value.merged ? 'merged' : value.state) : value.state === 'opened' ? 'open' : value.state,
|
|
73
|
+
draft: value.draft, url, headSha: candidate.headSha, title: value.title, body: github ? value.body : value.description });
|
|
74
|
+
}
|
|
75
|
+
async function branches(candidate) {
|
|
76
|
+
const project = identity(await get(''));
|
|
77
|
+
const branch = async name => {
|
|
78
|
+
let value;
|
|
79
|
+
try { value = await get(`${github ? '' : '/repository'}/branches/${encodeURIComponent(name)}`); }
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error?.status === 404) {
|
|
82
|
+
const failure = new Error('Publish the verified source branch and ensure the target branch exists before requesting review.');
|
|
83
|
+
failure.code = 'ERR_DELIVERY_BRANCH_NOT_PUBLISHED'; failure.safeMessage = failure.message; throw failure;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
ensure(value.name === name, 'changed-facts');
|
|
88
|
+
return sha(github ? value.commit?.sha : value.commit?.id);
|
|
89
|
+
};
|
|
90
|
+
const headSha = await branch(candidate.sourceBranch), baseSha = await branch(candidate.targetBranch);
|
|
91
|
+
ensure(headSha === candidate.headSha, 'changed-facts');
|
|
92
|
+
return { project, headSha, baseSha };
|
|
93
|
+
}
|
|
94
|
+
async function listed(candidate) {
|
|
95
|
+
const query = github
|
|
96
|
+
? `state=all&head=${encodeURIComponent(repository.namespace + ':' + candidate.sourceBranch)}&base=${encodeURIComponent(candidate.targetBranch)}`
|
|
97
|
+
: `state=all&source_branch=${encodeURIComponent(candidate.sourceBranch)}&target_branch=${encodeURIComponent(candidate.targetBranch)}`;
|
|
98
|
+
return http.paginate({ path: `${root}${reviewsPath}?${query}&per_page=100`, identityKey: github ? 'number' : 'id' });
|
|
99
|
+
}
|
|
100
|
+
async function observe(candidate) {
|
|
101
|
+
target(candidate);
|
|
102
|
+
const before = await branches(candidate), rows = await listed(candidate);
|
|
103
|
+
ensure(rows.length <= 1, 'ambiguous-review');
|
|
104
|
+
let existing = null;
|
|
105
|
+
if (rows.length) {
|
|
106
|
+
const selected = number(github ? rows[0].number : rows[0].iid);
|
|
107
|
+
existing = review(await get(`${reviewsPath}/${selected}`), candidate, before.project);
|
|
108
|
+
ensure(existing.number === selected, 'changed-facts');
|
|
109
|
+
if (candidate.reviewNumber !== null) ensure(existing.number === candidate.reviewNumber, 'changed-facts');
|
|
110
|
+
} else ensure(candidate.reviewNumber === null, 'changed-facts');
|
|
111
|
+
ensure(hash(await branches(candidate)) === hash(before), 'changed-facts');
|
|
112
|
+
return facts(candidate, before, existing);
|
|
113
|
+
}
|
|
114
|
+
function facts(candidate, before, existing) {
|
|
115
|
+
const unknown = { headSha: candidate.headSha, policy: 'unknown', satisfied: null, evidenceDigest: hash({ before, existing }) };
|
|
116
|
+
return plain({ repositoryUrl: repository.url, sourceBranch: candidate.sourceBranch, targetBranch: candidate.targetBranch,
|
|
117
|
+
headSha: candidate.headSha, baseSha: before.baseSha,
|
|
118
|
+
review: existing ? { number: existing.number, state: existing.state, url: existing.url, headSha: candidate.headSha } : null,
|
|
119
|
+
checks: unknown, reviews: unknown, observedAt: timestamp(clock()) });
|
|
120
|
+
}
|
|
121
|
+
function operation(value) {
|
|
122
|
+
const op = plain(value); target(op.candidate); digest(op.digest); digest(op.factsDigest);
|
|
123
|
+
ensure(op.action === 'review-request' && op.candidate.reviewNumber === null, 'unsupported-action');
|
|
124
|
+
reviewRequestContent(op);
|
|
125
|
+
return op;
|
|
126
|
+
}
|
|
127
|
+
function verified(value, op, project) {
|
|
128
|
+
const result = review(value, op.candidate, project), expected = reviewRequestContent(op);
|
|
129
|
+
ensure(!result.draft && result.title === expected.title && result.body === expected.body, 'unverified-effect');
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function receipt(op, result) {
|
|
133
|
+
return plain({ status: 'succeeded', operationDigest: op.digest, headSha: op.candidate.headSha,
|
|
134
|
+
evidenceDigest: hash(result), resourceUrl: result.url, commitSha: null });
|
|
135
|
+
}
|
|
136
|
+
async function findMarked(op, project) {
|
|
137
|
+
const marker = `<!-- rivet-review-operation:${op.digest} -->`;
|
|
138
|
+
const matches = (await listed(op.candidate)).filter(row => (github ? row.body : row.description)?.includes(marker));
|
|
139
|
+
ensure(matches.length === 1, 'ambiguous-review');
|
|
140
|
+
const selected = number(github ? matches[0].number : matches[0].iid);
|
|
141
|
+
const result = verified(await get(`${reviewsPath}/${selected}`), op, project);
|
|
142
|
+
ensure(result.number === selected, 'changed-facts');
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
async function dispatch(value, context) {
|
|
146
|
+
const op = operation(value), limits = plain(context); exact(limits, ['deadline']);
|
|
147
|
+
const deadline = Date.parse(timestamp(limits.deadline));
|
|
148
|
+
const timely = () => ensure(Date.parse(timestamp(clock())) < deadline, 'dispatch-expired');
|
|
149
|
+
timely();
|
|
150
|
+
const facts = await observe(op.candidate);
|
|
151
|
+
ensure(facts.review === null && factsDigest(facts) === op.factsDigest, 'changed-facts');
|
|
152
|
+
const content = reviewRequestContent(op);
|
|
153
|
+
const payload = github ? { title: content.title, body: content.body, head: op.candidate.sourceBranch, base: op.candidate.targetBranch, draft: false, maintainer_can_modify: false }
|
|
154
|
+
: { title: content.title, description: content.body, source_branch: op.candidate.sourceBranch, target_branch: op.candidate.targetBranch, remove_source_branch: false, squash: false };
|
|
155
|
+
const wireBody = createProviderWireBody({ provider: kind, action: 'review-request', resourceId: repository.fullName,
|
|
156
|
+
expectedState: 'absent', expectedVersion: op.candidate.headSha, idempotencyKey: op.digest, payload });
|
|
157
|
+
timely();
|
|
158
|
+
const controller = new AbortController(), remaining = deadline - Date.parse(timestamp(clock()));
|
|
159
|
+
ensure(remaining > 0, 'dispatch-expired');
|
|
160
|
+
const timer = setTimeout(() => controller.abort(), remaining);
|
|
161
|
+
let response;
|
|
162
|
+
try { response = await http.request({ method: 'POST', path: root + reviewsPath, wireBody, signal: controller.signal }); }
|
|
163
|
+
finally { clearTimeout(timer); }
|
|
164
|
+
ensure(response.status === 201, 'unverified-effect');
|
|
165
|
+
const after = await branches(op.candidate);
|
|
166
|
+
ensure(hash({ before: after, existing: null }) === facts.checks.evidenceDigest, 'changed-facts');
|
|
167
|
+
const sent = verified(response.data, op, after.project);
|
|
168
|
+
ensure(sent.state === 'open', 'unverified-effect');
|
|
169
|
+
const result = await findMarked(op, after.project);
|
|
170
|
+
ensure(hash(sent) === hash(result) && hash(await branches(op.candidate)) === hash(after), 'changed-facts');
|
|
171
|
+
return receipt(op, result);
|
|
172
|
+
}
|
|
173
|
+
async function reconcile(value) {
|
|
174
|
+
try {
|
|
175
|
+
const op = operation(value), before = await branches(op.candidate);
|
|
176
|
+
ensure(factsDigest(facts(op.candidate, before, null)) === op.factsDigest, 'changed-facts');
|
|
177
|
+
const result = await findMarked(op, before.project);
|
|
178
|
+
ensure(hash(await branches(op.candidate)) === hash(before), 'changed-facts');
|
|
179
|
+
return { status: 'succeeded', receipt: receipt(op, result) };
|
|
180
|
+
} catch { return { status: 'unknown' }; }
|
|
181
|
+
}
|
|
182
|
+
return createTrustedDeliveryExecutor({ provider: kind,
|
|
183
|
+
capabilities: [{ action: 'review-request', conditionalHead: false, verifiesCreatedReview: true, reconcile: true }], observe, dispatch, reconcile });
|
|
184
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { captureRecord, createProviderWireBody } from '../adapters/contract.js';
|
|
2
|
+
import { createProviderHttpClient } from '../adapters/http.js';
|
|
3
|
+
import { parseRepositoryRemote } from '../repositories/identity.js';
|
|
4
|
+
import { createTrustedDeliveryExecutor } from './service.js';
|
|
5
|
+
import { digest, ensure, exact, factsDigest, hash, plain, sha, timestamp, validateCandidate } from './contract.js';
|
|
6
|
+
|
|
7
|
+
const ENDPOINTS={github:'https://api.github.com',gitlab:'https://gitlab.com/api/v4',bitbucket:'https://api.bitbucket.org/2.0'};
|
|
8
|
+
const UUID=/^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\}$/i;
|
|
9
|
+
export function reviewUpdateContent(input, provider) {
|
|
10
|
+
const value=plain(input);exact(value,['title','body']);
|
|
11
|
+
ensure(typeof value.title==='string'&&value.title.trim()===value.title&&value.title.length>0&&Buffer.byteLength(value.title)<=256&&!/[\u0000-\u001f\u007f]/.test(value.title));
|
|
12
|
+
ensure(typeof value.body==='string'&&Buffer.byteLength(value.body)<=32000&&!/[\u0000-\u0008\u000b-\u001f\u007f]/.test(value.body));
|
|
13
|
+
// GitLab interprets these title prefixes and standalone slash commands as
|
|
14
|
+
// mutations when metadata is saved. Precheck them before any write; readback
|
|
15
|
+
// cannot undo a draft/closure/reviewer change caused by executable text.
|
|
16
|
+
if (provider === 'gitlab') {
|
|
17
|
+
ensure(!/^(?:(?:draft|wip):|\[(?:draft|wip)\]|\((?:draft|wip)\))/i.test(value.title),'unsupported-action');
|
|
18
|
+
ensure(!/^[ \t]*\/[a-z][a-z0-9_]*(?=[ \t]|$)/im.test(value.body),'unsupported-action');
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function markers(body){return body.match(/<!-- rivet-review-operation:[a-f0-9]{64} -->/g)??[];}
|
|
23
|
+
/** Desired-state confirmation, not an atomic metadata compare-and-swap. */
|
|
24
|
+
export function createReviewUpdateExecutor(inputConfig){
|
|
25
|
+
const input=captureRecord(inputConfig,new Set(['repository','reviewNumber','transport','headers','clock','timeoutMs']),['repository','reviewNumber','transport'],'invalid-config');
|
|
26
|
+
const repository=parseRepositoryRemote(input.repository.url);ensure(hash(repository)===hash(plain(input.repository)));
|
|
27
|
+
const kind=repository.provider,github=kind==='github',bitbucket=kind==='bitbucket';
|
|
28
|
+
ensure(Number.isSafeInteger(input.reviewNumber)&&input.reviewNumber>0&&input.reviewNumber<=1_000_000_000);
|
|
29
|
+
const selected=input.reviewNumber,clock=input.clock??(()=>new Date().toISOString());ensure(typeof clock==='function');
|
|
30
|
+
const root=github?`/repos/${repository.fullName}`:bitbucket?`/repositories/${repository.fullName}`:`/projects/${encodeURIComponent(repository.fullName)}`;
|
|
31
|
+
const path=`${root}/${github?'pulls':bitbucket?'pullrequests':'merge_requests'}/${selected}`;
|
|
32
|
+
const reviewUrl=`${repository.url}/${github?'pull':bitbucket?'pull-requests':'-/merge_requests'}/${selected}`;
|
|
33
|
+
const http=createProviderHttpClient({provider:kind,baseUrl:ENDPOINTS[kind],transport:input.transport,allowEncodedSlash:true,...(input.headers===undefined?{}:{headers:input.headers}),...(input.timeoutMs===undefined?{}:{timeoutMs:input.timeoutMs})});
|
|
34
|
+
const get=async path=>(await http.request({method:'GET',path})).data;
|
|
35
|
+
function target(value){validateCandidate(value);ensure(hash(value.repository)===hash(repository)&&(value.reviewNumber===null||value.reviewNumber===selected),'changed-facts');}
|
|
36
|
+
function identity(value){
|
|
37
|
+
if(bitbucket){ensure(typeof value?.uuid==='string'&&UUID.test(value.uuid)&&value.full_name===repository.fullName&&value.links?.html?.href===repository.url&&typeof value.workspace?.uuid==='string'&&UUID.test(value.workspace.uuid)&&value.workspace.slug===repository.namespace,'changed-facts');return {id:value.uuid,workspace:value.workspace.uuid};}
|
|
38
|
+
ensure(Number.isSafeInteger(value?.id)&&value.id>0&&(github?value.full_name:value.path_with_namespace)===repository.fullName&&(github?value.html_url:value.web_url)===repository.url&&value.archived===false,'changed-facts');return {id:value.id};
|
|
39
|
+
}
|
|
40
|
+
async function branches(candidate){
|
|
41
|
+
const project=identity(await get(root));
|
|
42
|
+
const branch=async name=>{const raw=await get(`${root}${github?'':bitbucket?'/refs':'/repository'}/branches/${encodeURIComponent(name)}`);ensure(raw?.name===name,'changed-facts');return sha(bitbucket?raw.target?.hash:github?raw.commit?.sha:raw.commit?.id);};
|
|
43
|
+
const headSha=await branch(candidate.sourceBranch),baseSha=await branch(candidate.targetBranch);ensure(headSha===candidate.headSha,'changed-facts');return {project,headSha,baseSha};
|
|
44
|
+
}
|
|
45
|
+
function normalize(raw,candidate,before){
|
|
46
|
+
ensure((github?raw.number:bitbucket?raw.id:raw.iid)===selected&&raw.draft===false&&(github?raw.state==='open'&&raw.merged===false:raw.state===(bitbucket?'OPEN':'opened')),'changed-facts');
|
|
47
|
+
ensure((github?raw.html_url:bitbucket?raw.links?.html?.href:raw.web_url)===reviewUrl,'changed-facts');
|
|
48
|
+
if(github){for(const [side,ref,commit] of [['head',candidate.sourceBranch,candidate.headSha],['base',candidate.targetBranch,before.baseSha]])ensure(raw[side]?.repo?.id===before.project.id&&raw[side].repo.full_name===repository.fullName&&raw[side].ref===ref&&raw[side].sha===commit,'changed-facts');}
|
|
49
|
+
else if(bitbucket){for(const [side,ref,commit] of [['source',candidate.sourceBranch,candidate.headSha],['destination',candidate.targetBranch,before.baseSha]]){const part=raw[side],repo=part?.repository;ensure(repo?.uuid===before.project.id&&repo.full_name===repository.fullName&&repo.links?.html?.href===repository.url&&part.branch?.name===ref&&part.commit?.hash===commit,'changed-facts');if(repo.workspace!==undefined)ensure(repo.workspace.uuid===before.project.workspace&&repo.workspace.slug===repository.namespace,'changed-facts');}}
|
|
50
|
+
else ensure(raw.project_id===before.project.id&&raw.source_project_id===before.project.id&&raw.target_project_id===before.project.id&&raw.source_branch===candidate.sourceBranch&&raw.target_branch===candidate.targetBranch&&raw.sha===candidate.headSha,'changed-facts');
|
|
51
|
+
const bodies=bitbucket?[raw.description,raw.summary?.raw,raw.rendered?.description?.raw].filter(x=>x!==undefined):[github?raw.body:raw.description];
|
|
52
|
+
ensure(bodies.length>0&&bodies.every(x=>x===bodies[0]),'invalid-provider-evidence');
|
|
53
|
+
const content=reviewUpdateContent({title:raw.title,body:bodies[0]??''});
|
|
54
|
+
return {number:selected,url:reviewUrl,state:'open',draft:false,...content};
|
|
55
|
+
}
|
|
56
|
+
async function snapshot(candidate){target(candidate);const before=await branches(candidate),review=normalize(await get(path),candidate,before);ensure(hash(await branches(candidate))===hash(before),'changed-facts');return plain({...before,review});}
|
|
57
|
+
function facts(candidate,current){const unknown={headSha:candidate.headSha,policy:'unknown',satisfied:null,evidenceDigest:hash(current)};return plain({repositoryUrl:repository.url,sourceBranch:candidate.sourceBranch,targetBranch:candidate.targetBranch,headSha:candidate.headSha,baseSha:current.baseSha,review:{number:selected,state:'open',url:reviewUrl,headSha:candidate.headSha},checks:unknown,reviews:unknown,observedAt:timestamp(clock())});}
|
|
58
|
+
async function observe(candidate){return facts(candidate,await snapshot(candidate));}
|
|
59
|
+
async function prepare(candidate,desired){let content=reviewUpdateContent(desired,kind);const before=await snapshot(candidate),existing=markers(before.review.body);if(!markers(content.body).length&&existing.length)content=reviewUpdateContent({...content,body:content.body+'\n\n'+existing.join('\n')},kind);ensure(hash(markers(content.body))===hash(existing),'changed-facts');return plain({assurance:'precheck-readback',before,desired:content});}
|
|
60
|
+
function operation(value){const op=plain(value);target(op.candidate);digest(op.digest);digest(op.factsDigest);ensure(op.action==='review-update','unsupported-action');exact(op.payload,['assurance','before','desired']);ensure(op.payload.assurance==='precheck-readback');exact(op.payload.before,['project','headSha','baseSha','review']);ensure(op.payload.before.headSha===op.candidate.headSha);sha(op.payload.before.baseSha);const review=op.payload.before.review;exact(review,['number','url','state','draft','title','body']);ensure(review.number===selected&&review.url===reviewUrl&&review.state==='open'&&review.draft===false);reviewUpdateContent({title:review.title,body:review.body});reviewUpdateContent(op.payload.desired,kind);ensure(hash(markers(review.body))===hash(markers(op.payload.desired.body)),'changed-facts');ensure(factsDigest(facts(op.candidate,op.payload.before))===op.factsDigest,'changed-facts');return op;}
|
|
61
|
+
function verified(current,op){const expected={...op.payload.before,review:{...op.payload.before.review,...op.payload.desired}};ensure(hash(current)===hash(expected),'unverified-effect');return current;}
|
|
62
|
+
function receipt(op,current){return plain({status:'succeeded',operationDigest:op.digest,headSha:op.candidate.headSha,evidenceDigest:hash({assurance:'precheck-readback',current}),resourceUrl:reviewUrl,commitSha:null});}
|
|
63
|
+
async function dispatch(value,context){const op=operation(value);exact(plain(context),['deadline']);const deadline=Date.parse(timestamp(context.deadline));const timely=()=>ensure(Date.parse(timestamp(clock()))<deadline,'dispatch-expired');timely();const before=await snapshot(op.candidate);ensure(hash(before)===hash(op.payload.before),'changed-facts');
|
|
64
|
+
const payload=github?op.payload.desired:{title:op.payload.desired.title,description:op.payload.desired.body};
|
|
65
|
+
const wireBody=createProviderWireBody({provider:kind,action:'review-update',resourceId:`${repository.fullName}#${selected}`,expectedState:'open',expectedVersion:op.candidate.headSha,idempotencyKey:op.digest,payload});
|
|
66
|
+
timely();const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),deadline-Date.parse(timestamp(clock())));let response;
|
|
67
|
+
try{response=await http.request({method:github?'PATCH':'PUT',path,wireBody,signal:controller.signal});}finally{clearTimeout(timer);}
|
|
68
|
+
ensure(response.status===200,'unverified-effect');normalize(response.data,op.candidate,before);const current=verified(await snapshot(op.candidate),op);return receipt(op,current);
|
|
69
|
+
}
|
|
70
|
+
async function reconcile(value){try{const op=operation(value),current=verified(await snapshot(op.candidate),op);return {status:'succeeded',receipt:receipt(op,current)};}catch{return {status:'unknown'};}}
|
|
71
|
+
const executor=createTrustedDeliveryExecutor({provider:kind,capabilities:[{action:'review-update',conditionalHead:false,verifiesDesiredState:true,reconcile:true}],observe,dispatch,reconcile});
|
|
72
|
+
// Preserve the trusted executor identity while exposing a separate read-only preview builder.
|
|
73
|
+
return Object.freeze({executor,prepare,observe:executor.observe,dispatch:executor.dispatch,reconcile:executor.reconcile,capabilities:executor.capabilities});
|
|
74
|
+
}
|