forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
4
|
+
const { performance } = require('node:perf_hooks');
|
|
5
|
+
const { types } = require('node:util');
|
|
6
|
+
const {
|
|
7
|
+
canonicalize,
|
|
8
|
+
computeContentHash,
|
|
9
|
+
validateContract,
|
|
10
|
+
validateContractStructure,
|
|
11
|
+
} = require('@forge/contracts');
|
|
12
|
+
|
|
13
|
+
const WORK_PACKET_SCHEMA = 'forge.memory.work-packet.v1';
|
|
14
|
+
const RUN_RECEIPT_SCHEMA = 'forge.memory.run-receipt.v1';
|
|
15
|
+
const HEAD_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
16
|
+
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
17
|
+
const MAX_SNAPSHOT_DEPTH = 10;
|
|
18
|
+
const MAX_SNAPSHOT_NODES = 2_000;
|
|
19
|
+
const MAX_SNAPSHOT_BYTES = 65_536;
|
|
20
|
+
const DEFAULT_PROVIDER_TIMEOUT_MS = 5_000;
|
|
21
|
+
const PROVIDER_DEADLINE_SENTINEL = Symbol('provider-deadline');
|
|
22
|
+
const MAX_PROVIDER_TIMEOUT_MS = 30_000;
|
|
23
|
+
const MAX_TRACE_SCAN_ROWS = 128;
|
|
24
|
+
const MAX_TRACE_ITERATIONS = 128;
|
|
25
|
+
const MAX_TRACE_TOTAL_ITERATIONS = 128;
|
|
26
|
+
const MAX_READY_ITEMS = 128;
|
|
27
|
+
const SECRET_PATTERNS = Object.freeze([
|
|
28
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/i,
|
|
29
|
+
/sk_(?:live|test)_[A-Za-z0-9]{16,}/i,
|
|
30
|
+
/sk-(?:live|test)_[A-Za-z0-9]{16,}/i,
|
|
31
|
+
/sk-[A-Za-z0-9]{16,}/i,
|
|
32
|
+
/AKIA[0-9A-Z]{16}/i,
|
|
33
|
+
/(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,}/i,
|
|
34
|
+
]);
|
|
35
|
+
const USER_PATH_PATTERN = /(?:[A-Za-z]:\\Users\\[^\\\s]+|\/(?:Users|home)\/[^/\s]+(?=\/|\s|$))/i;
|
|
36
|
+
|
|
37
|
+
// Complete accepted provider capability contract. Live probes may supply the optional reads;
|
|
38
|
+
// runIssueOperation, recordPrLinkage, and readTrace remain mandatory (see authority-provider.js).
|
|
39
|
+
const PR_LIFECYCLE_PROVIDER_METHODS = Object.freeze([
|
|
40
|
+
'readIssue',
|
|
41
|
+
'readOwnership',
|
|
42
|
+
'readHead',
|
|
43
|
+
'readCapability',
|
|
44
|
+
'readRisk',
|
|
45
|
+
'readGates',
|
|
46
|
+
'listReadyWork',
|
|
47
|
+
'runIssueOperation',
|
|
48
|
+
'recordPrLinkage',
|
|
49
|
+
'recordOpenedPrLinkage',
|
|
50
|
+
'readTrace',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function isCanonicalGitCommonDir(value) {
|
|
54
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512) return false;
|
|
55
|
+
const normalized = value.replaceAll('\\', '/');
|
|
56
|
+
if (normalized.includes('%') || [...normalized].some(character => {
|
|
57
|
+
const code = character.charCodeAt(0);
|
|
58
|
+
return code < 0x20 || code === 0x7f;
|
|
59
|
+
})) return false;
|
|
60
|
+
const segments = normalized.split('/');
|
|
61
|
+
const segmentStart = /^[A-Za-z]:$/.test(segments[0]) || segments[0] === '' ? 1 : 0;
|
|
62
|
+
if (segments.slice(segmentStart).some(segment => segment.length === 0 || segment.trim() !== segment)) return false;
|
|
63
|
+
if (segments.some(segment => segment === '.' || segment === '..' || containsSecret(segment))) return false;
|
|
64
|
+
if (segments.at(-1)?.toLowerCase() !== '.git') return false;
|
|
65
|
+
const posixAbsolute = segments[0] === '' && segments.length >= 3;
|
|
66
|
+
const windowsAbsolute = /^[a-z]:$/i.test(segments[0]) && segments.length >= 3;
|
|
67
|
+
return posixAbsolute || windowsAbsolute;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class PrLifecycleAuthorityError extends Error {
|
|
71
|
+
constructor(code, message, options = {}) {
|
|
72
|
+
super(message, Object.hasOwn(options, 'cause') ? { cause: options.cause } : undefined);
|
|
73
|
+
this.name = 'PrLifecycleAuthorityError';
|
|
74
|
+
this.code = code;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fail(code, message, options) {
|
|
79
|
+
throw new PrLifecycleAuthorityError(code, message, options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function containsSecret(value) {
|
|
83
|
+
return typeof value === 'string' && SECRET_PATTERNS.some((pattern) => pattern.test(value));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function containsForbidden(value, options = {}, context = '') {
|
|
87
|
+
if (typeof value === 'string') {
|
|
88
|
+
return containsSecret(value) || USER_PATH_PATTERN.test(value);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(value)) return value.some(item => containsForbidden(item, options, context));
|
|
91
|
+
if (value && typeof value === 'object') {
|
|
92
|
+
return Object.entries(value).some(([key, nested]) => {
|
|
93
|
+
if (containsSecret(key) || USER_PATH_PATTERN.test(key)) return true;
|
|
94
|
+
const allowsGitCommonDir = key === 'git_common_dir'
|
|
95
|
+
&& (options.allowGitCommonDir === true
|
|
96
|
+
|| options.allowGitCommonDir === 'target' && context === 'target');
|
|
97
|
+
if (allowsGitCommonDir) return !isCanonicalGitCommonDir(nested);
|
|
98
|
+
return containsForbidden(nested, options, key === 'target' ? 'target' : '');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isForbiddenKey(key) {
|
|
105
|
+
return containsSecret(key) || USER_PATH_PATTERN.test(key);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function redactForbidden(value, options = {}, context = '') {
|
|
109
|
+
if (typeof value === 'string') return containsForbidden(value, options, context) ? '[redacted]' : value;
|
|
110
|
+
if (Array.isArray(value)) return value.map(item => redactForbidden(item, options, context));
|
|
111
|
+
if (value && typeof value === 'object') {
|
|
112
|
+
return Object.fromEntries(Object.entries(value)
|
|
113
|
+
.filter(([key]) => !isForbiddenKey(key))
|
|
114
|
+
.map(([key, item]) => {
|
|
115
|
+
const allowsGitCommonDir = key === 'git_common_dir'
|
|
116
|
+
&& (options.allowGitCommonDir === true
|
|
117
|
+
|| options.allowGitCommonDir === 'target' && context === 'target');
|
|
118
|
+
return [key, allowsGitCommonDir && isCanonicalGitCommonDir(item)
|
|
119
|
+
? item
|
|
120
|
+
: redactForbidden(item, options, key === 'target' ? 'target' : '')];
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function snapshot(value, label, options = {}) {
|
|
127
|
+
try {
|
|
128
|
+
const serialized = canonicalize(value, {
|
|
129
|
+
maxDepth: MAX_SNAPSHOT_DEPTH,
|
|
130
|
+
maxNodes: MAX_SNAPSHOT_NODES,
|
|
131
|
+
maxBytes: MAX_SNAPSHOT_BYTES,
|
|
132
|
+
});
|
|
133
|
+
const bounded = JSON.parse(serialized);
|
|
134
|
+
if (options.privacy !== 'sanitize' && containsForbidden(bounded, options)) {
|
|
135
|
+
fail('PR_LIFECYCLE_PRIVACY_REJECTED', `${label} contains a secret or absolute user path`);
|
|
136
|
+
}
|
|
137
|
+
return options.privacy === 'sanitize' ? redactForbidden(bounded, options) : bounded;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof PrLifecycleAuthorityError) throw error;
|
|
140
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', `${label} must be bounded plain JSON`, { cause: error });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function requiredString(value, label) {
|
|
145
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512) {
|
|
146
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', `${label} must be a bounded non-empty string`);
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function requiredHead(value, label) {
|
|
152
|
+
if (typeof value !== 'string' || !HEAD_PATTERN.test(value)) {
|
|
153
|
+
fail('PR_LIFECYCLE_HEAD_STALE', `${label} is not a valid exact head`);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requiredLinkageHead(value) {
|
|
159
|
+
requiredHead(value, 'PR linkage exact head');
|
|
160
|
+
if (value.length !== 40) fail('PR_LIFECYCLE_HEAD_STALE', 'PR linkage requires a 40-character commit head');
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function requiredHash(value, label) {
|
|
165
|
+
if (typeof value !== 'string' || !HASH_PATTERN.test(value)) {
|
|
166
|
+
fail('PR_LIFECYCLE_CONTRACT_INVALID', `${label} is not a valid digest`);
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class ProviderDeadlineError extends Error {}
|
|
172
|
+
|
|
173
|
+
function validateProviderTimeout(timeoutMs) {
|
|
174
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_PROVIDER_TIMEOUT_MS) {
|
|
175
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', `provider timeout must be an integer from 1 to ${MAX_PROVIDER_TIMEOUT_MS} milliseconds`);
|
|
176
|
+
}
|
|
177
|
+
return timeoutMs;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function invokeWithDeadline(target, method, args, timeoutMs, options = {}) {
|
|
181
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
182
|
+
const methodArgs = [...args];
|
|
183
|
+
// Cancellation is best-effort for backward compatibility: established provider
|
|
184
|
+
// methods do not accept an options object, so only explicit signal parameters opt in.
|
|
185
|
+
if (controller && target[method]?.length > args.length) methodArgs.push(controller.signal);
|
|
186
|
+
let timer;
|
|
187
|
+
let timedOut = false;
|
|
188
|
+
const operation = Promise.resolve().then(() => target[method](...methodArgs));
|
|
189
|
+
const deadline = new Promise((resolve, reject) => {
|
|
190
|
+
timer = setTimeout(() => {
|
|
191
|
+
timedOut = true;
|
|
192
|
+
if (options.reconcileOnTimeout) resolve(PROVIDER_DEADLINE_SENTINEL);
|
|
193
|
+
else {
|
|
194
|
+
controller?.abort();
|
|
195
|
+
reject(new ProviderDeadlineError('provider operation deadline exceeded'));
|
|
196
|
+
}
|
|
197
|
+
}, timeoutMs);
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
const result = await Promise.race([operation, deadline]);
|
|
201
|
+
return result;
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
if (timedOut && !options.reconcileOnTimeout) controller?.abort();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function callMethod(target, method, args, label, options = {}) {
|
|
209
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
const invocationArgs = options.sanitize
|
|
213
|
+
? snapshot(args, `${label} provider input`, { privacy: 'sanitize', allowGitCommonDir: options.allowGitCommonDir })
|
|
214
|
+
: args;
|
|
215
|
+
result = await invokeWithDeadline(target, method, invocationArgs, timeoutMs, options);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof PrLifecycleAuthorityError) throw error;
|
|
218
|
+
fail('PR_LIFECYCLE_UNAVAILABLE', `${label} provider operation failed`, { cause: error });
|
|
219
|
+
}
|
|
220
|
+
// Consequential writes reconcile this private deadline through a bounded,
|
|
221
|
+
// authoritative trace read instead of waiting on an unacknowledged write.
|
|
222
|
+
if (result === PROVIDER_DEADLINE_SENTINEL) return result;
|
|
223
|
+
try {
|
|
224
|
+
const projected = typeof options.project === 'function' ? options.project(result) : result;
|
|
225
|
+
const bounded = snapshot(projected, `${label} provider response`, {
|
|
226
|
+
privacy: options.sanitize ? 'sanitize' : undefined,
|
|
227
|
+
allowGitCommonDir: options.allowGitCommonDir,
|
|
228
|
+
});
|
|
229
|
+
if (options.unwrap && bounded?.ok === true && bounded.data !== undefined) return bounded.data;
|
|
230
|
+
if (options.unwrap && bounded?.ok === false) fail('PR_LIFECYCLE_UNAVAILABLE', `${label} provider rejected operation`);
|
|
231
|
+
return bounded;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof PrLifecycleAuthorityError) throw error;
|
|
234
|
+
fail('PR_LIFECYCLE_UNAVAILABLE', `${label} provider operation failed`, { cause: error });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function callProbe(provider, probes, names, args, label, fallbackName, timeoutMs, options = {}) {
|
|
239
|
+
const injected = names.find((name) => typeof probes?.[name] === 'function');
|
|
240
|
+
if (injected) return callMethod(probes, injected, args, label, { timeoutMs, ...options });
|
|
241
|
+
const direct = names.find((name) => typeof provider[name] === 'function');
|
|
242
|
+
if (direct) return callMethod(provider, direct, args, label, { timeoutMs, ...options });
|
|
243
|
+
// Issue read/ownership may use the public operation broker. No invented live-state
|
|
244
|
+
// operation names are permitted for head, capability, risk, or gate evidence.
|
|
245
|
+
if (fallbackName && typeof provider.runIssueOperation === 'function') {
|
|
246
|
+
let operationArgs = args;
|
|
247
|
+
let context = {};
|
|
248
|
+
if (fallbackName === 'owns' && args[0] && typeof args[0] === 'object') {
|
|
249
|
+
operationArgs = [args[0].issue_id];
|
|
250
|
+
context = { actor: args[0].actor_id, sessionId: args[0].session_id };
|
|
251
|
+
}
|
|
252
|
+
return callMethod(provider, 'runIssueOperation', [fallbackName, operationArgs, context], label, { timeoutMs, unwrap: true, ...options });
|
|
253
|
+
}
|
|
254
|
+
fail('PR_LIFECYCLE_UNAVAILABLE', `${label} live probe is unavailable`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function callOwnershipProbe(provider, probes, ownershipArgs, timeoutMs) {
|
|
258
|
+
if (typeof probes?.readOwnership === 'function') return callMethod(probes, 'readOwnership', [ownershipArgs], 'ownership', { timeoutMs });
|
|
259
|
+
if (typeof provider.readOwnership === 'function') return callMethod(provider, 'readOwnership', [ownershipArgs], 'ownership', { timeoutMs });
|
|
260
|
+
const context = { actor: ownershipArgs.actor_id, sessionId: ownershipArgs.session_id };
|
|
261
|
+
const owns = await callMethod(provider, 'runIssueOperation', ['owns', [ownershipArgs.issue_id], context], 'ownership', { timeoutMs, unwrap: true });
|
|
262
|
+
if (owns?.owned !== true) return owns;
|
|
263
|
+
const claims = await callMethod(provider, 'runIssueOperation', ['claims', [], context], 'claims', { timeoutMs, unwrap: true });
|
|
264
|
+
const matchingClaims = Array.isArray(claims?.claims) ? claims.claims.filter(claim => claim?.issue_id === ownershipArgs.issue_id
|
|
265
|
+
&& claim?.actor === ownershipArgs.actor_id && claim?.session_id === ownershipArgs.session_id) : [];
|
|
266
|
+
if (matchingClaims.length !== 1) fail('PR_LIFECYCLE_OWNERSHIP_STALE', 'live ownership session evidence is unavailable or ambiguous');
|
|
267
|
+
return { ...owns, actor_id: owns.actor ?? owns.claimed_by, session_id: matchingClaims[0].session_id };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function providerIssueRevision(issue) {
|
|
271
|
+
const revision = issue?.revision ?? issue?.issue_revision;
|
|
272
|
+
if (!Number.isInteger(revision) || revision < 0) fail('PR_LIFECYCLE_REVISION_STALE', 'live issue revision is unavailable');
|
|
273
|
+
return revision;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function canonicalRepositoryId(value) {
|
|
277
|
+
const repositoryId = requiredString(value, 'repository_id').trim().toLowerCase();
|
|
278
|
+
if (!/^[a-z0-9_.-]+(?:\/[a-z0-9_.-]+)+$/.test(repositoryId)) {
|
|
279
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', 'repository_id must be a canonical owner/repository identifier');
|
|
280
|
+
}
|
|
281
|
+
return repositoryId;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function providerHead(head, repositoryId) {
|
|
285
|
+
const value = typeof head === 'string' ? { head } : head;
|
|
286
|
+
const exactHead = value?.head ?? value?.exact_head ?? value?.target_head;
|
|
287
|
+
requiredLinkageHead(exactHead);
|
|
288
|
+
const repo = canonicalRepositoryId(value?.repository_id ?? value?.repository ?? repositoryId);
|
|
289
|
+
if (repositoryId && repo !== canonicalRepositoryId(repositoryId)) fail('PR_LIFECYCLE_HEAD_STALE', 'live repository does not match packet');
|
|
290
|
+
return { head: exactHead, repository_id: repo };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function assertLiveIssue(issue, expectedIssueId) {
|
|
294
|
+
const id = issue?.id ?? issue?.issue_id;
|
|
295
|
+
if (id !== expectedIssueId) fail('PR_LIFECYCLE_READINESS_STALE', 'live issue identity does not match packet');
|
|
296
|
+
const revision = providerIssueRevision(issue);
|
|
297
|
+
if (issue.blocked === true || issue.blockers > 0) {
|
|
298
|
+
fail('PR_LIFECYCLE_READINESS_STALE', 'issue is not live-ready');
|
|
299
|
+
}
|
|
300
|
+
return revision;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function readReadyIssue(provider, issueId, actorId, sessionId, timeoutMs) {
|
|
304
|
+
const input = { issue_id: issueId, actor_id: actorId, session_id: sessionId };
|
|
305
|
+
const response = typeof provider.listReadyWork === 'function'
|
|
306
|
+
? await callMethod(provider, 'listReadyWork', [input], 'ready', { timeoutMs, project: projectReadyResponse })
|
|
307
|
+
: await callMethod(provider, 'runIssueOperation', ['ready', [], { actor: actorId, sessionId }], 'ready', {
|
|
308
|
+
timeoutMs, unwrap: true, project: projectReadyResponse,
|
|
309
|
+
});
|
|
310
|
+
const ready = Array.isArray(response) ? response : response?.issues;
|
|
311
|
+
if (!Array.isArray(ready)) fail('PR_LIFECYCLE_UNAVAILABLE', 'ready provider returned a malformed queue');
|
|
312
|
+
const matches = ready.filter(issue => (issue?.id ?? issue?.issue_id) === issueId);
|
|
313
|
+
if (matches.length !== 1) fail('PR_LIFECYCLE_READINESS_STALE', 'issue is absent or ambiguous in the authoritative ready queue');
|
|
314
|
+
return matches[0];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 0.1 limitation: LeaseReceipt/lease_epoch is deferred. Same actor/session release and
|
|
318
|
+
// reacquire is ABA-indistinguishable, so every operation re-probes ownership and receipts
|
|
319
|
+
// never confer continuing authority.
|
|
320
|
+
function assertOwnership(ownership, expected = {}) {
|
|
321
|
+
if (!ownership || ownership.owned !== true || ownership.active === false) {
|
|
322
|
+
fail('PR_LIFECYCLE_OWNERSHIP_STALE', 'live ownership is not held');
|
|
323
|
+
}
|
|
324
|
+
const actorId = ownership.actor_id ?? ownership.claimed_by;
|
|
325
|
+
if (expected.actor_id && actorId !== expected.actor_id) {
|
|
326
|
+
fail('PR_LIFECYCLE_OWNERSHIP_STALE', 'live ownership actor mismatch');
|
|
327
|
+
}
|
|
328
|
+
if (expected.session_id && ownership.session_id !== expected.session_id) {
|
|
329
|
+
fail('PR_LIFECYCLE_OWNERSHIP_STALE', 'live ownership session mismatch');
|
|
330
|
+
}
|
|
331
|
+
requiredString(actorId, 'live ownership actor_id');
|
|
332
|
+
return { ...ownership, actor_id: actorId };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function capabilityDigest(capability) {
|
|
336
|
+
const digest = capability?.manifest_digest ?? capability?.digest ?? capability?.capability_manifest_digest;
|
|
337
|
+
requiredHash(digest, 'live capability manifest digest');
|
|
338
|
+
if (capability.approved !== true || capability.available !== true || capability.probed !== true) {
|
|
339
|
+
fail('PR_LIFECYCLE_CAPABILITY_INVALID', 'live capability must be approved, available, and positively probed');
|
|
340
|
+
}
|
|
341
|
+
const expiresAt = capability.expires_at ?? capability.expiresAt;
|
|
342
|
+
if (typeof expiresAt !== 'string' || !Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now()) {
|
|
343
|
+
fail('PR_LIFECYCLE_CAPABILITY_INVALID', 'live capability is expired or missing expiry evidence');
|
|
344
|
+
}
|
|
345
|
+
return digest;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function assertRisk(risk, expectedDigest) {
|
|
349
|
+
const approved = risk?.approved === true || risk?.status === 'approved' || risk?.status === 'PASS';
|
|
350
|
+
if (!approved) fail('PR_LIFECYCLE_RISK_INVALID', 'live risk evidence is incomplete or unapproved');
|
|
351
|
+
const digest = risk?.risk_manifest_digest ?? risk?.digest;
|
|
352
|
+
requiredHash(digest, 'live risk manifest digest');
|
|
353
|
+
if (expectedDigest && digest !== expectedDigest) fail('PR_LIFECYCLE_RISK_INVALID', 'live risk digest does not match packet');
|
|
354
|
+
return { approved: true, digest };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function assertGates(gates) {
|
|
358
|
+
const value = Array.isArray(gates) ? { gates } : gates;
|
|
359
|
+
const approved = value?.approved === true || value?.status === 'approved' || value?.status === 'PASS';
|
|
360
|
+
const complete = value?.complete === true || value?.complete === undefined && Array.isArray(value?.gates);
|
|
361
|
+
if (!approved || !complete || value?.conflict === true || value?.stale === true) {
|
|
362
|
+
fail('PR_LIFECYCLE_GATE_INVALID', 'live gates are incomplete, stale, or conflicting');
|
|
363
|
+
}
|
|
364
|
+
const ids = Array.isArray(value.ids) ? value.ids : Array.isArray(value.gate_ids) ? value.gate_ids : [];
|
|
365
|
+
if (ids.length === 0 || ids.some((id) => typeof id !== 'string' || id.length === 0)) {
|
|
366
|
+
fail('PR_LIFECYCLE_GATE_INVALID', 'live gate ids are malformed');
|
|
367
|
+
}
|
|
368
|
+
return { approved: true, complete: true, ids: [...ids] };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function liveValidationCode(errors) {
|
|
372
|
+
const codes = new Set(errors.map(item => item.code));
|
|
373
|
+
if (codes.has('STALE_EXACT_HEAD')) return 'PR_LIFECYCLE_HEAD_STALE';
|
|
374
|
+
if (codes.has('STALE_ISSUE_REVISION')) return 'PR_LIFECYCLE_REVISION_STALE';
|
|
375
|
+
if (codes.has('WRONG_CAPABILITY_DIGEST')) return 'PR_LIFECYCLE_CAPABILITY_INVALID';
|
|
376
|
+
if (codes.has('STALE_WORKFLOW_CONFIG')) return 'PR_LIFECYCLE_REVISION_STALE';
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function assertContract(value, schema, expected) {
|
|
381
|
+
const structural = validateContractStructure(value);
|
|
382
|
+
if (!structural.ok || value?.schema_id !== schema) fail('PR_LIFECYCLE_CONTRACT_INVALID', `${schema} contract is malformed`);
|
|
383
|
+
const result = expected === undefined ? structural : validateContract(value, { expected });
|
|
384
|
+
if (!result.ok) {
|
|
385
|
+
const liveCode = liveValidationCode(result.errors);
|
|
386
|
+
if (liveCode) fail(liveCode, `${schema} live binding is stale`);
|
|
387
|
+
const error = new PrLifecycleAuthorityError('PR_LIFECYCLE_CONTRACT_INVALID', `${schema} contract failed live validation`);
|
|
388
|
+
error.errors = result.errors;
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function uuidFromSeed(seed) {
|
|
395
|
+
const hash = createHash('sha256').update(seed).digest('hex');
|
|
396
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function optionalArrayField(input, field) {
|
|
400
|
+
if (input[field] === undefined) return {};
|
|
401
|
+
if (!Array.isArray(input[field])) fail('PR_LIFECYCLE_INVALID_INPUT', `${field} must be an array`);
|
|
402
|
+
return { [field]: [...input[field]] };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function optionalPlainObjectField(input, field) {
|
|
406
|
+
if (input[field] === undefined) return {};
|
|
407
|
+
if (!input[field] || typeof input[field] !== 'object' || Array.isArray(input[field])) {
|
|
408
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', `${field} must be a plain object`);
|
|
409
|
+
}
|
|
410
|
+
return { [field]: { ...input[field] } };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function buildPacket(input, live) {
|
|
414
|
+
const issueId = requiredString(input.issue_id ?? input.issueId, 'issue_id');
|
|
415
|
+
const repositoryId = canonicalRepositoryId(input.repository_id ?? input.repositoryId ?? live.head.repository_id);
|
|
416
|
+
const packetId = requiredString(input.packet_id ?? input.packetId ?? `packet-${issueId}-${live.issueRevision}-${live.head.head}`, 'packet_id');
|
|
417
|
+
const createdAt = input.created_at ?? input.createdAt ?? new Date().toISOString();
|
|
418
|
+
const objective = requiredString(input.objective ?? live.issue.objective ?? 'PR lifecycle operation', 'objective');
|
|
419
|
+
const allowedMutations = input.allowed_mutations ?? input.allowedMutations ?? ['pr.opened'];
|
|
420
|
+
if (!Array.isArray(allowedMutations) || allowedMutations.length === 0 || allowedMutations.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
421
|
+
fail('PR_LIFECYCLE_MUTATION_UNAUTHORIZED', 'allowed mutations are malformed');
|
|
422
|
+
}
|
|
423
|
+
const packet = {
|
|
424
|
+
schema_id: WORK_PACKET_SCHEMA,
|
|
425
|
+
schema_version: 1,
|
|
426
|
+
object_id: input.object_id ?? input.objectId ?? uuidFromSeed(`${issueId}:${packetId}:${live.head.head}`),
|
|
427
|
+
created_at: createdAt,
|
|
428
|
+
producer: {
|
|
429
|
+
product_id: input.product_id ?? 'forge-memory',
|
|
430
|
+
product_version: input.product_version ?? '0.1.0-beta.6',
|
|
431
|
+
instance_id: input.instance_id ?? input.instanceId ?? randomUUID(),
|
|
432
|
+
},
|
|
433
|
+
capabilities_used: Array.isArray(input.capabilities_used) ? input.capabilities_used : [],
|
|
434
|
+
provenance: {
|
|
435
|
+
source_kind: 'kernel', actor_class: 'agent', actor_id: live.ownership.actor_id,
|
|
436
|
+
},
|
|
437
|
+
payload: {
|
|
438
|
+
issue_id: issueId,
|
|
439
|
+
expected_issue_revision: live.issueRevision,
|
|
440
|
+
packet_id: packetId,
|
|
441
|
+
packet_revision: Number.isInteger(input.packet_revision ?? input.packetRevision) ? input.packet_revision ?? input.packetRevision : 1,
|
|
442
|
+
repository_id: repositoryId,
|
|
443
|
+
target_head: live.head.head,
|
|
444
|
+
objective,
|
|
445
|
+
authority: { kind: 'kernel', issue_revision: live.issueRevision },
|
|
446
|
+
allowed_mutations: [...allowedMutations],
|
|
447
|
+
workflow_config_revision: live.workflowConfigRevision,
|
|
448
|
+
capability_manifest_digest: live.capabilityDigest,
|
|
449
|
+
...optionalArrayField(input, 'acceptance_criteria'),
|
|
450
|
+
...optionalArrayField(input, 'prohibited_actions'),
|
|
451
|
+
risk: input.risk ?? live.risk,
|
|
452
|
+
...(live.risk.digest ? { risk_manifest_digest: live.risk.digest } : {}),
|
|
453
|
+
...(input.target ? { target: input.target } : {}),
|
|
454
|
+
receipt_requirements: {
|
|
455
|
+
...optionalPlainObjectField(input, 'receipt_requirements').receipt_requirements,
|
|
456
|
+
terminal: true,
|
|
457
|
+
gate_ids: [...live.gates.ids],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
extensions: {},
|
|
461
|
+
};
|
|
462
|
+
packet.content_hash = computeContentHash(packet);
|
|
463
|
+
return packet;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function assertReceiptEvidence(packet, receipt) {
|
|
467
|
+
if (receipt.payload.status !== 'PASS') fail('PR_LIFECYCLE_TERMINAL_INVALID', 'RunReceipt is not terminal PASS');
|
|
468
|
+
const attempted = receipt.payload.mutations_attempted ?? [];
|
|
469
|
+
const authorized = receipt.payload.mutations_authorized ?? [];
|
|
470
|
+
const allowed = packet.payload.allowed_mutations ?? [];
|
|
471
|
+
const prohibited = packet.payload.prohibited_actions ?? [];
|
|
472
|
+
const attemptedSet = new Set(attempted);
|
|
473
|
+
const authorizedSet = new Set(authorized);
|
|
474
|
+
const sameMutations = attemptedSet.size === attempted.length
|
|
475
|
+
&& authorizedSet.size === authorized.length
|
|
476
|
+
&& attemptedSet.size === authorizedSet.size
|
|
477
|
+
&& attemptedSet.size > 0
|
|
478
|
+
&& [...attemptedSet].every(mutation => authorizedSet.has(mutation));
|
|
479
|
+
const opened = allowed.includes('pr.opened')
|
|
480
|
+
&& attemptedSet.has('pr.opened')
|
|
481
|
+
&& authorizedSet.has('pr.opened');
|
|
482
|
+
if (!opened || !sameMutations || attempted.some((entry) => entry !== 'pr.opened'
|
|
483
|
+
|| !allowed.includes(entry) || prohibited.includes(entry))) {
|
|
484
|
+
fail('PR_LIFECYCLE_MUTATION_UNAUTHORIZED', 'RunReceipt mutation evidence is incomplete or incompatible');
|
|
485
|
+
}
|
|
486
|
+
if (receipt.payload.lease_epoch !== undefined) fail('PR_LIFECYCLE_AUTHORITY_UNSUPPORTED', 'lease_epoch is deferred for 0.1');
|
|
487
|
+
if (receipt.payload.validation?.status !== 'PASS' || receipt.payload.cleanup?.status !== 'PASS') fail('PR_LIFECYCLE_TERMINAL_INVALID', 'RunReceipt terminal validation or cleanup is incomplete');
|
|
488
|
+
if (!Array.isArray(receipt.payload.evidence_refs) || receipt.payload.evidence_refs.length === 0) fail('PR_LIFECYCLE_TERMINAL_INVALID', 'RunReceipt terminal evidence is missing');
|
|
489
|
+
return 'opened';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function assertReceiptExecutor(receipt) {
|
|
493
|
+
const provenance = receipt.provenance;
|
|
494
|
+
const producer = receipt.producer;
|
|
495
|
+
if (producer?.product_id !== 'forge-flow'
|
|
496
|
+
|| provenance?.source_kind !== 'execution'
|
|
497
|
+
|| provenance?.actor_class !== 'system'
|
|
498
|
+
|| provenance?.actor_id !== producer?.instance_id) {
|
|
499
|
+
fail('PR_LIFECYCLE_CONTRACT_INVALID', 'RunReceipt executor provenance is not trusted');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function authenticateReceipt(receiptVerifier, packet, receipt, timeoutMs) {
|
|
504
|
+
const identity = {
|
|
505
|
+
receipt_hash: receipt.content_hash,
|
|
506
|
+
packet_hash: packet.content_hash,
|
|
507
|
+
run_id: receipt.payload.run_id,
|
|
508
|
+
attempt_id: receipt.payload.attempt_id,
|
|
509
|
+
producer_instance_id: receipt.producer.instance_id,
|
|
510
|
+
};
|
|
511
|
+
const verification = await callMethod({ verify: receiptVerifier }, 'verify', [identity], 'receipt authentication', { timeoutMs });
|
|
512
|
+
if (verification?.authenticated !== true
|
|
513
|
+
|| verification.receipt_hash !== identity.receipt_hash
|
|
514
|
+
|| verification.packet_hash !== identity.packet_hash
|
|
515
|
+
|| verification.run_id !== identity.run_id
|
|
516
|
+
|| verification.attempt_id !== identity.attempt_id
|
|
517
|
+
|| verification.producer_instance_id !== identity.producer_instance_id) {
|
|
518
|
+
fail('PR_LIFECYCLE_CONTRACT_INVALID', 'RunReceipt lacks authenticated Flow execution evidence');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function assertPacketLiveBindings(packet, live) {
|
|
523
|
+
const requiredGateIds = packet.payload.receipt_requirements?.gate_ids;
|
|
524
|
+
if (!Array.isArray(requiredGateIds) || requiredGateIds.length === 0 || requiredGateIds.some((id) => typeof id !== 'string' || id.length === 0) || new Set(requiredGateIds).size !== requiredGateIds.length) fail('PR_LIFECYCLE_GATE_INVALID', 'WorkPacket gate requirements are incomplete');
|
|
525
|
+
if (requiredGateIds.length !== live.gates.ids.length || requiredGateIds.some((id) => !live.gates.ids.includes(id))) fail('PR_LIFECYCLE_GATE_INVALID', 'WorkPacket gate requirements do not match live gates');
|
|
526
|
+
if (!HASH_PATTERN.test(packet.payload.risk_manifest_digest || '') || packet.payload.risk_manifest_digest !== live.risk.digest) fail('PR_LIFECYCLE_RISK_INVALID', 'WorkPacket risk digest does not match live risk');
|
|
527
|
+
const prohibited = packet.payload.prohibited_actions ?? [];
|
|
528
|
+
if (!Array.isArray(prohibited) || packet.payload.allowed_mutations.some(mutation => prohibited.includes(mutation))) fail('PR_LIFECYCLE_MUTATION_UNAUTHORIZED', 'WorkPacket allowed and prohibited mutations conflict');
|
|
529
|
+
if (packet.payload.allowed_mutations.length !== 1 || packet.payload.allowed_mutations[0] !== 'pr.opened') fail('PR_LIFECYCLE_MUTATION_UNAUTHORIZED', 'WorkPacket may authorize only pr.opened receipt linkage');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function deriveLinkage(packet, receipt, gates) {
|
|
533
|
+
const target = packet.payload.target ?? {};
|
|
534
|
+
const prEvidence = receipt.payload.evidence_refs.filter(entry => entry?.kind === 'pr' && Number.isInteger(entry?.pr_number));
|
|
535
|
+
if (prEvidence.length !== 1 || prEvidence[0].pr_number <= 0 || typeof prEvidence[0].url !== 'string') {
|
|
536
|
+
fail('PR_LIFECYCLE_LINKAGE_UNAVAILABLE', 'opened receipt must contain exactly one complete PR evidence reference');
|
|
537
|
+
}
|
|
538
|
+
const evidenceRepository = prEvidence[0].repository_id ?? prEvidence[0].repository ?? prEvidence[0].repo;
|
|
539
|
+
if (evidenceRepository !== undefined && evidenceRepository !== packet.payload.repository_id) {
|
|
540
|
+
fail('PR_LIFECYCLE_LINKAGE_CONFLICT', 'receipt PR repository conflicts with packet authority');
|
|
541
|
+
}
|
|
542
|
+
if (Number.isInteger(target.pr_number) && prEvidence.some(entry => entry.pr_number !== target.pr_number)) {
|
|
543
|
+
fail('PR_LIFECYCLE_LINKAGE_CONFLICT', 'receipt PR number conflicts with packet target');
|
|
544
|
+
}
|
|
545
|
+
let prNumber = Number.isInteger(target.pr_number) ? target.pr_number : undefined;
|
|
546
|
+
if (prNumber === undefined) prNumber = prEvidence[0]?.pr_number;
|
|
547
|
+
const matchingEvidence = prEvidence;
|
|
548
|
+
if (typeof target.url === 'string' && matchingEvidence.some(entry => entry.url !== target.url)) {
|
|
549
|
+
fail('PR_LIFECYCLE_LINKAGE_CONFLICT', 'receipt PR URL conflicts with packet target');
|
|
550
|
+
}
|
|
551
|
+
const evidenceUrl = matchingEvidence[0]?.url;
|
|
552
|
+
return {
|
|
553
|
+
issue_id: packet.payload.issue_id,
|
|
554
|
+
repository_id: packet.payload.repository_id,
|
|
555
|
+
head: packet.payload.target_head,
|
|
556
|
+
...(prNumber === undefined ? {} : { pr_number: prNumber }),
|
|
557
|
+
...(typeof target.url === 'string' || evidenceUrl ? { url: target.url ?? evidenceUrl } : {}),
|
|
558
|
+
...(typeof target.git_common_dir === 'string' ? { git_common_dir: target.git_common_dir } : {}),
|
|
559
|
+
gate_ids: [...gates.ids],
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function packetLinkage(packet, gates) {
|
|
564
|
+
const target = packet.payload.target ?? {};
|
|
565
|
+
return {
|
|
566
|
+
issue_id: packet.payload.issue_id,
|
|
567
|
+
repository_id: packet.payload.repository_id,
|
|
568
|
+
head: packet.payload.target_head,
|
|
569
|
+
...(Number.isInteger(target.pr_number) ? { pr_number: target.pr_number } : {}),
|
|
570
|
+
...(typeof target.url === 'string' ? { url: target.url } : {}),
|
|
571
|
+
...(typeof target.git_common_dir === 'string' ? { git_common_dir: target.git_common_dir } : {}),
|
|
572
|
+
gate_ids: [...gates.ids],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function traceItems(trace) {
|
|
577
|
+
const items = [];
|
|
578
|
+
const requests = Array.isArray(trace?.pull_requests) ? trace.pull_requests : [];
|
|
579
|
+
for (const request of requests) {
|
|
580
|
+
const iterations = Array.isArray(request.iterations) ? request.iterations : [];
|
|
581
|
+
items.push(...iterations.map((iteration) => ({ ...iteration, request })));
|
|
582
|
+
}
|
|
583
|
+
for (const list of [trace?.iterations, trace?.accepted, trace?.accepted_receipts]) {
|
|
584
|
+
if (Array.isArray(list)) items.push(...list);
|
|
585
|
+
}
|
|
586
|
+
return items;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function itemPacketHash(item) {
|
|
590
|
+
return item.work_packet_hash ?? item.packet_hash ?? item.packet?.content_hash;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function itemReceiptHash(item) {
|
|
594
|
+
return item.run_receipt_hash ?? item.receipt_hash ?? item.receipt?.content_hash;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function packetSemanticIdentity(packet) {
|
|
598
|
+
const payload = packet?.payload;
|
|
599
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
600
|
+
return JSON.stringify([
|
|
601
|
+
payload.issue_id,
|
|
602
|
+
payload.expected_issue_revision,
|
|
603
|
+
payload.packet_id,
|
|
604
|
+
payload.packet_revision,
|
|
605
|
+
payload.repository_id,
|
|
606
|
+
payload.target_head,
|
|
607
|
+
]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function itemPacketIdentity(item) {
|
|
611
|
+
return item.work_packet_identity ?? item.packet_identity ?? packetSemanticIdentity(item.packet);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function durableAcceptance(trace, packet, receipt) {
|
|
615
|
+
const items = traceItems(trace);
|
|
616
|
+
const exact = items.find((item) => itemPacketHash(item) === packet.content_hash && itemReceiptHash(item) === receipt.content_hash);
|
|
617
|
+
if (exact) return exact;
|
|
618
|
+
const identity = packetSemanticIdentity(packet);
|
|
619
|
+
const conflict = items.find((item) => itemPacketHash(item) === packet.content_hash
|
|
620
|
+
|| identity && itemPacketIdentity(item) === identity
|
|
621
|
+
|| item.receipt?.payload?.run_id === receipt.payload.run_id
|
|
622
|
+
|| item.run_id === receipt.payload.run_id);
|
|
623
|
+
if (conflict) fail('PR_LIFECYCLE_REPLAY_CONFLICT', 'durable trace conflicts with accepted content');
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function dataProperty(value, key) {
|
|
628
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
629
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
630
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) return undefined;
|
|
631
|
+
return descriptor.value;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function projectTraceIteration(iteration) {
|
|
635
|
+
const packet = dataProperty(iteration, 'packet');
|
|
636
|
+
const receipt = dataProperty(iteration, 'receipt');
|
|
637
|
+
const receiptPayload = dataProperty(receipt, 'payload');
|
|
638
|
+
const packetHash = dataProperty(iteration, 'work_packet_hash')
|
|
639
|
+
?? dataProperty(iteration, 'packet_hash')
|
|
640
|
+
?? dataProperty(packet, 'content_hash');
|
|
641
|
+
const receiptHash = dataProperty(iteration, 'run_receipt_hash')
|
|
642
|
+
?? dataProperty(iteration, 'receipt_hash')
|
|
643
|
+
?? dataProperty(receipt, 'content_hash');
|
|
644
|
+
const runId = dataProperty(iteration, 'run_id') ?? dataProperty(receiptPayload, 'run_id');
|
|
645
|
+
const packetIdentity = dataProperty(iteration, 'work_packet_identity')
|
|
646
|
+
?? dataProperty(iteration, 'packet_identity')
|
|
647
|
+
?? packetSemanticIdentity(packet);
|
|
648
|
+
return {
|
|
649
|
+
...(dataProperty(iteration, 'type') === undefined ? {} : { type: dataProperty(iteration, 'type') }),
|
|
650
|
+
...(packetHash === undefined ? {} : { work_packet_hash: packetHash }),
|
|
651
|
+
...(receiptHash === undefined ? {} : { run_receipt_hash: receiptHash }),
|
|
652
|
+
...(runId === undefined ? {} : { run_id: runId }),
|
|
653
|
+
...(packetIdentity === null || packetIdentity === undefined ? {} : { work_packet_identity: packetIdentity }),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function projectReadyResponse(value) {
|
|
658
|
+
if (types.isProxy(value)) throw new TypeError('ready provider response cannot be a Proxy');
|
|
659
|
+
const ok = dataProperty(value, 'ok');
|
|
660
|
+
const data = dataProperty(value, 'data');
|
|
661
|
+
const envelope = ok === true && data && !Array.isArray(data) ? data : value;
|
|
662
|
+
if (types.isProxy(envelope)) throw new TypeError('ready provider envelope cannot be a Proxy');
|
|
663
|
+
const issues = Array.isArray(envelope) ? envelope : dataProperty(envelope, 'issues');
|
|
664
|
+
if (!Array.isArray(issues)) return value;
|
|
665
|
+
if (types.isProxy(issues)) throw new TypeError('ready provider queue cannot be a Proxy');
|
|
666
|
+
if (issues.length > MAX_READY_ITEMS) fail('PR_LIFECYCLE_READINESS_STALE', 'authoritative ready queue exceeds lifecycle bounds');
|
|
667
|
+
const selected = [];
|
|
668
|
+
const length = issues.length;
|
|
669
|
+
for (let index = 0; index < length; index += 1) {
|
|
670
|
+
const descriptor = Object.getOwnPropertyDescriptor(issues, String(index));
|
|
671
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
|
|
672
|
+
throw new TypeError('ready provider queue must contain plain dense entries');
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
canonicalize([...selected, descriptor.value], {
|
|
676
|
+
maxDepth: MAX_SNAPSHOT_DEPTH,
|
|
677
|
+
maxNodes: MAX_SNAPSHOT_NODES,
|
|
678
|
+
maxBytes: MAX_SNAPSHOT_BYTES - 1_024,
|
|
679
|
+
});
|
|
680
|
+
selected.push(descriptor.value);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (['CANONICAL_BYTE_LIMIT', 'CANONICAL_NODE_LIMIT', 'CANONICAL_DEPTH_LIMIT'].includes(error?.code)) {
|
|
683
|
+
fail('PR_LIFECYCLE_READINESS_STALE', 'authoritative ready queue exceeds lifecycle bounds');
|
|
684
|
+
}
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return ok === true
|
|
689
|
+
? { ok: true, data: selected }
|
|
690
|
+
: selected;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function projectIssueResponse(value) {
|
|
694
|
+
if (types.isProxy(value)) throw new TypeError('issue provider response cannot be a Proxy');
|
|
695
|
+
const ok = dataProperty(value, 'ok');
|
|
696
|
+
const data = dataProperty(value, 'data');
|
|
697
|
+
const issue = ok === true ? data : value;
|
|
698
|
+
if (!issue || typeof issue !== 'object' || Array.isArray(issue) || types.isProxy(issue)) return value;
|
|
699
|
+
const projected = {};
|
|
700
|
+
for (const key of ['id', 'issue_id', 'revision', 'issue_revision', 'status', 'state', 'ready', 'is_ready',
|
|
701
|
+
'blocked', 'blockers', 'readiness_state', 'objective']) {
|
|
702
|
+
const field = dataProperty(issue, key);
|
|
703
|
+
if (field !== undefined) projected[key] = field;
|
|
704
|
+
}
|
|
705
|
+
if (containsForbidden(projected)) {
|
|
706
|
+
fail('PR_LIFECYCLE_PRIVACY_REJECTED', 'issue provider response contains a secret or absolute user path');
|
|
707
|
+
}
|
|
708
|
+
return ok === true ? { ok: true, data: projected } : projected;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function projectTrace(trace, target) {
|
|
712
|
+
const requests = dataProperty(trace, 'pull_requests');
|
|
713
|
+
if (!Array.isArray(requests)) return { pull_requests: requests };
|
|
714
|
+
const gapEvidence = dataProperty(trace, 'gaps');
|
|
715
|
+
if (gapEvidence !== undefined && !Array.isArray(gapEvidence)) fail('PR_LIFECYCLE_INVALID_INPUT', 'public PR trace gap evidence is malformed');
|
|
716
|
+
const gaps = gapEvidence ?? [];
|
|
717
|
+
if (gaps.some(gap => typeof gap === 'string'
|
|
718
|
+
&& (gap === 'pull_requests:overflow'
|
|
719
|
+
|| /^pull_requests:.*:unlinked_issue$/.test(gap)
|
|
720
|
+
|| /^iterations:.*:(?:missing|incomplete|overflow)$/.test(gap)))) {
|
|
721
|
+
fail('PR_LIFECYCLE_REPLAY_CONFLICT', 'public PR trace authority evidence is incomplete');
|
|
722
|
+
}
|
|
723
|
+
if (requests.length > MAX_TRACE_SCAN_ROWS) {
|
|
724
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', 'public PR trace rows exceed lifecycle bounds');
|
|
725
|
+
}
|
|
726
|
+
const matches = [];
|
|
727
|
+
let totalIterations = 0;
|
|
728
|
+
for (const request of requests) {
|
|
729
|
+
if (dataProperty(request, 'issue_id') !== target.issue_id
|
|
730
|
+
|| dataProperty(request, 'repo') !== target.repository_id) continue;
|
|
731
|
+
const iterations = dataProperty(request, 'iterations');
|
|
732
|
+
totalIterations += Array.isArray(iterations) ? iterations.length : 0;
|
|
733
|
+
if (!Array.isArray(iterations) || iterations.length > MAX_TRACE_ITERATIONS
|
|
734
|
+
|| totalIterations > MAX_TRACE_TOTAL_ITERATIONS) {
|
|
735
|
+
fail('PR_LIFECYCLE_INVALID_INPUT', 'public PR trace iterations exceed lifecycle bounds');
|
|
736
|
+
}
|
|
737
|
+
matches.push({
|
|
738
|
+
number: dataProperty(request, 'number'),
|
|
739
|
+
repo: dataProperty(request, 'repo'),
|
|
740
|
+
...(typeof dataProperty(request, 'git_common_dir') === 'string'
|
|
741
|
+
? { git_common_dir: dataProperty(request, 'git_common_dir') }
|
|
742
|
+
: {}),
|
|
743
|
+
head_sha: dataProperty(request, 'head_sha'),
|
|
744
|
+
issue_id: dataProperty(request, 'issue_id'),
|
|
745
|
+
iterations: iterations.map(projectTraceIteration),
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return { pull_requests: matches };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function traceTarget(linkage, gitCommonDir) {
|
|
752
|
+
return {
|
|
753
|
+
issue_id: linkage.issue_id,
|
|
754
|
+
repository_id: linkage.repository_id,
|
|
755
|
+
repo: linkage.repository_id,
|
|
756
|
+
...(Number.isInteger(linkage.pr_number) ? { pr_number: linkage.pr_number } : {}),
|
|
757
|
+
...(typeof gitCommonDir === 'string' ? { git_common_dir: gitCommonDir } : {}),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function readLifecycleTrace(provider, linkage, timeoutMs, gitCommonDir) {
|
|
762
|
+
const target = traceTarget(linkage, gitCommonDir);
|
|
763
|
+
return callMethod(provider, 'readTrace', [target], 'trace', {
|
|
764
|
+
timeoutMs,
|
|
765
|
+
allowGitCommonDir: true,
|
|
766
|
+
project: value => projectTrace(value, target),
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function reconcileLifecycleTrace(provider, linkage, packet, receipt, timeoutMs, gitCommonDir) {
|
|
771
|
+
const budgetMs = Math.max(timeoutMs * 2, 10);
|
|
772
|
+
const startedAt = performance.now();
|
|
773
|
+
const remainingBudget = () => budgetMs - (performance.now() - startedAt);
|
|
774
|
+
let trace;
|
|
775
|
+
do {
|
|
776
|
+
const remaining = Math.max(1, remainingBudget());
|
|
777
|
+
try {
|
|
778
|
+
trace = await readLifecycleTrace(provider, linkage, Math.min(timeoutMs, remaining), gitCommonDir);
|
|
779
|
+
traceLinkageRow(trace, linkage);
|
|
780
|
+
if (durableAcceptance(trace, packet, receipt)) {
|
|
781
|
+
assertTraceLinkage(trace, linkage, packet, receipt);
|
|
782
|
+
return trace;
|
|
783
|
+
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (!(error instanceof PrLifecycleAuthorityError)
|
|
786
|
+
|| error.code !== 'PR_LIFECYCLE_UNAVAILABLE' || remainingBudget() <= 0) throw error;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const waitMs = Math.min(5, remainingBudget());
|
|
790
|
+
if (waitMs > 0) await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
791
|
+
} while (remainingBudget() > 0);
|
|
792
|
+
assertTraceLinkage(trace, linkage, packet, receipt);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function traceLinkageRow(trace, linkage) {
|
|
796
|
+
if (!trace || typeof trace !== 'object' || Array.isArray(trace)) fail('PR_LIFECYCLE_UNAVAILABLE', 'public PR trace is unavailable');
|
|
797
|
+
if (!Number.isInteger(linkage.pr_number) || linkage.pr_number <= 0) fail('PR_LIFECYCLE_LINKAGE_UNAVAILABLE', 'accepted packet lacks authoritative PR linkage');
|
|
798
|
+
if (!Array.isArray(trace.pull_requests)) fail('PR_LIFECYCLE_LINKAGE_UNAVAILABLE', 'public PR trace has no durable pull-request linkage');
|
|
799
|
+
const numbered = trace.pull_requests.filter((entry) => Number(entry?.number) === linkage.pr_number);
|
|
800
|
+
const candidates = linkage.git_common_dir !== undefined && numbered.some(entry => typeof entry?.git_common_dir === 'string')
|
|
801
|
+
? numbered.filter(entry => entry.git_common_dir === linkage.git_common_dir)
|
|
802
|
+
: numbered;
|
|
803
|
+
if (candidates.length > 1) fail('PR_LIFECYCLE_LINKAGE_CONFLICT', 'public PR trace contains ambiguous duplicate PR rows');
|
|
804
|
+
if (candidates.length === 0) return null;
|
|
805
|
+
const match = candidates[0];
|
|
806
|
+
if (match.repo !== linkage.repository_id || match.head_sha !== linkage.head || match.issue_id !== linkage.issue_id) fail('PR_LIFECYCLE_LINKAGE_CONFLICT', 'public PR trace conflicts with accepted linkage');
|
|
807
|
+
return match;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function assertTraceLinkage(trace, linkage, packet, receipt) {
|
|
811
|
+
const match = traceLinkageRow(trace, linkage);
|
|
812
|
+
if (!match) fail('PR_LIFECYCLE_LINKAGE_CONFLICT', 'public PR trace does not contain the accepted PR');
|
|
813
|
+
if (!durableAcceptance({ pull_requests: [match] }, packet, receipt)) fail('PR_LIFECYCLE_NOT_ACCEPTED', 'public PR trace lacks accepted packet and receipt evidence');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function stableResult(kind, packet, receipt, linkage) {
|
|
817
|
+
const { git_common_dir: _gitCommonDir, ...publicLinkage } = linkage;
|
|
818
|
+
return { accepted: true, packet_hash: packet.content_hash, receipt_hash: receipt.content_hash, run_id: receipt.payload.run_id, attempt_id: receipt.payload.attempt_id, linkage: publicLinkage };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function createPrLifecycleAuthority({ provider, liveProbes = {}, receiptVerifier, timeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS } = {}) {
|
|
822
|
+
if (!provider || typeof provider !== 'object' || Array.isArray(provider)) throw new TypeError('PR lifecycle authority requires a public provider object');
|
|
823
|
+
validateProviderTimeout(timeoutMs);
|
|
824
|
+
for (const method of ['runIssueOperation', 'recordPrLinkage', 'recordOpenedPrLinkage', 'readTrace']) {
|
|
825
|
+
if (typeof provider[method] !== 'function') throw new TypeError(`PR lifecycle provider must implement ${method}()`);
|
|
826
|
+
}
|
|
827
|
+
if (!liveProbes || typeof liveProbes !== 'object' || Array.isArray(liveProbes)) throw new TypeError('liveProbes must be an object');
|
|
828
|
+
if (typeof receiptVerifier !== 'function') throw new TypeError('receiptVerifier must be a function');
|
|
829
|
+
async function readLive(input, packet = null) {
|
|
830
|
+
const issueId = requiredString(input.issue_id ?? input.issueId ?? packet?.payload.issue_id, 'issue_id');
|
|
831
|
+
const repositoryId = input.repository_id ?? input.repositoryId ?? packet?.payload.repository_id;
|
|
832
|
+
const expectedActor = packet?.provenance?.actor_id;
|
|
833
|
+
if (packet?.provenance && Object.hasOwn(packet.provenance, 'session_id')) {
|
|
834
|
+
fail('PR_LIFECYCLE_CONTRACT_INVALID', 'WorkPacket provenance cannot contain session_id');
|
|
835
|
+
}
|
|
836
|
+
const requestedActor = packet ? expectedActor : requiredString(input.actor_id ?? input.actorId, 'actor_id');
|
|
837
|
+
const requestedSession = requiredString(input.session_id ?? input.sessionId, 'session_id');
|
|
838
|
+
const issue = await callProbe(provider, liveProbes, ['readIssue'], [issueId], 'issue', 'show', timeoutMs, { project: projectIssueResponse });
|
|
839
|
+
const issueRevision = assertLiveIssue(issue, issueId);
|
|
840
|
+
const readyIssue = await readReadyIssue(provider, issueId, requestedActor, requestedSession, timeoutMs);
|
|
841
|
+
const readyRevision = readyIssue.revision ?? readyIssue.issue_revision;
|
|
842
|
+
if (readyRevision !== undefined && readyRevision !== issueRevision) {
|
|
843
|
+
fail('PR_LIFECYCLE_REVISION_STALE', 'authoritative ready queue revision does not match live issue');
|
|
844
|
+
}
|
|
845
|
+
const ownershipArgs = { issue_id: issueId, ...(requestedActor ? { actor_id: requestedActor } : {}), ...(requestedSession ? { session_id: requestedSession } : {}) };
|
|
846
|
+
const ownership = assertOwnership(
|
|
847
|
+
await callOwnershipProbe(provider, liveProbes, ownershipArgs, timeoutMs),
|
|
848
|
+
{ actor_id: requestedActor, session_id: requestedSession },
|
|
849
|
+
);
|
|
850
|
+
const head = await callProbe(provider, liveProbes, ['readHead'], [{ issue_id: issueId, repository_id: repositoryId }], 'head', undefined, timeoutMs);
|
|
851
|
+
const normalizedHead = providerHead(head, repositoryId);
|
|
852
|
+
const capability = await callProbe(provider, liveProbes, ['readCapability'], [{ issue_id: issueId, repository_id: normalizedHead.repository_id }], 'capability', undefined, timeoutMs);
|
|
853
|
+
const capabilityDigestValue = capabilityDigest(capability);
|
|
854
|
+
const workflowConfigRevision = requiredString(capability.workflow_config_revision ?? capability.config_revision, 'workflow_config_revision');
|
|
855
|
+
if (input.workflow_config_revision && input.workflow_config_revision !== workflowConfigRevision) fail('PR_LIFECYCLE_REVISION_STALE', 'requested workflow config revision is stale');
|
|
856
|
+
const risk = await callProbe(provider, liveProbes, ['readRisk'], [{ issue_id: issueId }], 'risk', undefined, timeoutMs);
|
|
857
|
+
const normalizedRisk = assertRisk(risk, packet?.payload.risk_manifest_digest);
|
|
858
|
+
const gates = await callProbe(provider, liveProbes, ['readGates'], [{ issue_id: issueId }], 'gates', undefined, timeoutMs);
|
|
859
|
+
const normalizedGates = assertGates(gates);
|
|
860
|
+
return { issue, issueRevision, ownership, head: normalizedHead, capability, capabilityDigest: capabilityDigestValue, risk: normalizedRisk, gates: normalizedGates, workflowConfigRevision };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function assertCallerBinding(input, packet) {
|
|
864
|
+
if (input.actor_id && input.actor_id !== packet.provenance.actor_id) fail('PR_LIFECYCLE_OWNERSHIP_STALE', 'caller actor does not match packet provenance');
|
|
865
|
+
if (input.actorId && input.actorId !== packet.provenance.actor_id) fail('PR_LIFECYCLE_OWNERSHIP_STALE', 'caller actor does not match packet provenance');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function issueWorkPacket(rawInput = {}) {
|
|
869
|
+
const input = snapshot(rawInput, 'issue input', { allowGitCommonDir: 'target' });
|
|
870
|
+
requiredString(input.actor_id ?? input.actorId, 'actor_id');
|
|
871
|
+
optionalPlainObjectField(input, 'receipt_requirements');
|
|
872
|
+
const live = await readLive(input);
|
|
873
|
+
const packet = buildPacket(input, live);
|
|
874
|
+
assertContract(packet, WORK_PACKET_SCHEMA, { issueRevision: live.issueRevision, workflowConfigRevision: live.workflowConfigRevision, capabilityManifestDigest: live.capabilityDigest, exactHead: live.head.head });
|
|
875
|
+
assertPacketLiveBindings(packet, live);
|
|
876
|
+
return { packet: snapshot(packet, 'WorkPacket', { allowGitCommonDir: 'target' }), linkage: packetLinkage(packet, live.gates) };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function acceptRunReceipt(rawInput = {}) {
|
|
880
|
+
const input = snapshot(rawInput, 'receipt input', { allowGitCommonDir: 'target' });
|
|
881
|
+
const packet = snapshot(input.packet, 'WorkPacket', { allowGitCommonDir: 'target' });
|
|
882
|
+
const receipt = snapshot(input.receipt, 'RunReceipt');
|
|
883
|
+
assertContract(packet, WORK_PACKET_SCHEMA);
|
|
884
|
+
assertContract(receipt, RUN_RECEIPT_SCHEMA);
|
|
885
|
+
assertCallerBinding(input, packet);
|
|
886
|
+
const live = await readLive({ ...input, issue_id: packet.payload.issue_id }, packet);
|
|
887
|
+
assertContract(packet, WORK_PACKET_SCHEMA, { issueRevision: live.issueRevision, workflowConfigRevision: live.workflowConfigRevision, capabilityManifestDigest: live.capabilityDigest, exactHead: live.head.head });
|
|
888
|
+
assertPacketLiveBindings(packet, live);
|
|
889
|
+
assertContract(receipt, RUN_RECEIPT_SCHEMA, { packetHash: packet.content_hash, workflowConfigRevision: packet.payload.workflow_config_revision, capabilityManifestDigest: packet.payload.capability_manifest_digest, exactHead: packet.payload.target_head });
|
|
890
|
+
assertReceiptEvidence(packet, receipt);
|
|
891
|
+
assertReceiptExecutor(receipt);
|
|
892
|
+
await authenticateReceipt(receiptVerifier, packet, receipt, timeoutMs);
|
|
893
|
+
const linkage = deriveLinkage(packet, receipt, live.gates);
|
|
894
|
+
const target = packet.payload.target;
|
|
895
|
+
if (!target || !Number.isInteger(linkage.pr_number) || linkage.pr_number <= 0
|
|
896
|
+
|| typeof target.branch !== 'string' || typeof target.git_common_dir !== 'string'
|
|
897
|
+
|| typeof linkage.url !== 'string') {
|
|
898
|
+
fail('PR_LIFECYCLE_LINKAGE_UNAVAILABLE', 'receipt acceptance requires authoritative PR linkage fields');
|
|
899
|
+
}
|
|
900
|
+
const trace = await readLifecycleTrace(provider, linkage, timeoutMs, target.git_common_dir);
|
|
901
|
+
traceLinkageRow(trace, linkage);
|
|
902
|
+
const durable = durableAcceptance(trace, packet, receipt);
|
|
903
|
+
if (durable) assertTraceLinkage(trace, linkage, packet, receipt);
|
|
904
|
+
else {
|
|
905
|
+
const persistence = await callMethod(provider, 'recordOpenedPrLinkage', [{
|
|
906
|
+
phase: 'opened',
|
|
907
|
+
git_common_dir: target.git_common_dir,
|
|
908
|
+
repo: linkage.repository_id,
|
|
909
|
+
number: linkage.pr_number,
|
|
910
|
+
branch: target.branch,
|
|
911
|
+
url: linkage.url,
|
|
912
|
+
occurred_at: receipt.payload.ended_at ?? receipt.created_at,
|
|
913
|
+
work_packet: packet,
|
|
914
|
+
run_receipt: receipt,
|
|
915
|
+
}, { actor: packet.provenance.actor_id, sessionId: input.session_id ?? input.sessionId }], 'PR linkage', {
|
|
916
|
+
sanitize: true, allowGitCommonDir: true, timeoutMs, reconcileOnTimeout: true,
|
|
917
|
+
});
|
|
918
|
+
if (persistence === PROVIDER_DEADLINE_SENTINEL) {
|
|
919
|
+
await reconcileLifecycleTrace(provider, linkage, packet, receipt, timeoutMs, target.git_common_dir);
|
|
920
|
+
} else {
|
|
921
|
+
const persistedTrace = await readLifecycleTrace(provider, linkage, timeoutMs, target.git_common_dir);
|
|
922
|
+
assertTraceLinkage(persistedTrace, linkage, packet, receipt);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return stableResult('accepted', packet, receipt, linkage);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function requestNextWork(rawInput = {}) {
|
|
929
|
+
const input = snapshot(rawInput, 'next work input');
|
|
930
|
+
const method = typeof provider.listReadyWork === 'function' ? 'listReadyWork' : undefined;
|
|
931
|
+
const response = method
|
|
932
|
+
? await callMethod(provider, method, [input], 'ready', { timeoutMs, project: projectReadyResponse })
|
|
933
|
+
: await callMethod(provider, 'runIssueOperation', ['ready', [], {
|
|
934
|
+
actor: input.actor_id ?? input.actorId,
|
|
935
|
+
sessionId: input.session_id ?? input.sessionId,
|
|
936
|
+
worktreeId: input.worktree_id ?? input.worktreeId,
|
|
937
|
+
}], 'ready', { timeoutMs, unwrap: true, project: projectReadyResponse });
|
|
938
|
+
const ready = Array.isArray(response) ? response : response?.issues;
|
|
939
|
+
if (!Array.isArray(ready)) fail('PR_LIFECYCLE_UNAVAILABLE', 'ready provider returned a malformed queue');
|
|
940
|
+
return ready;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return Object.freeze({ issueWorkPacket, acceptRunReceipt, requestNextWork });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
module.exports = { PR_LIFECYCLE_PROVIDER_METHODS, PrLifecycleAuthorityError, createPrLifecycleAuthority };
|