cool-workflow 0.1.78
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/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/mcp.json +10 -0
- package/.codex-plugin/plugin.json +38 -0
- package/.mcp.json +10 -0
- package/LICENSE +24 -0
- package/README.md +638 -0
- package/apps/architecture-review/app.json +51 -0
- package/apps/architecture-review/workflow.js +116 -0
- package/apps/end-to-end-golden-path/app.json +30 -0
- package/apps/end-to-end-golden-path/workflow.js +33 -0
- package/apps/pr-review-fix-ci/app.json +59 -0
- package/apps/pr-review-fix-ci/workflow.js +90 -0
- package/apps/release-cut/app.json +54 -0
- package/apps/release-cut/workflow.js +82 -0
- package/apps/research-synthesis/app.json +50 -0
- package/apps/research-synthesis/workflow.js +76 -0
- package/apps/workflow-app-framework-demo/app.json +29 -0
- package/apps/workflow-app-framework-demo/workflow.js +44 -0
- package/dist/agent-config.js +223 -0
- package/dist/candidate-scoring.js +715 -0
- package/dist/capability-core.js +630 -0
- package/dist/capability-dispatcher.js +86 -0
- package/dist/capability-registry.js +523 -0
- package/dist/cli.js +1276 -0
- package/dist/collaboration.js +727 -0
- package/dist/commit.js +570 -0
- package/dist/contract-migration.js +234 -0
- package/dist/coordinator.js +1163 -0
- package/dist/daemon.js +44 -0
- package/dist/dispatch.js +201 -0
- package/dist/drive.js +503 -0
- package/dist/error-feedback.js +415 -0
- package/dist/evidence-grounding.js +179 -0
- package/dist/evidence-reasoning.js +733 -0
- package/dist/execution-backend.js +1279 -0
- package/dist/harness.js +61 -0
- package/dist/mcp-server.js +1615 -0
- package/dist/multi-agent-eval.js +857 -0
- package/dist/multi-agent-host.js +764 -0
- package/dist/multi-agent-operator-ux.js +537 -0
- package/dist/multi-agent-trust.js +366 -0
- package/dist/multi-agent.js +1173 -0
- package/dist/node-snapshot.js +270 -0
- package/dist/observability.js +922 -0
- package/dist/operator-ux.js +971 -0
- package/dist/orchestrator/audit-operations.js +182 -0
- package/dist/orchestrator/candidate-operations.js +117 -0
- package/dist/orchestrator/cli-options.js +288 -0
- package/dist/orchestrator/collaboration-operations.js +86 -0
- package/dist/orchestrator/feedback-operations.js +81 -0
- package/dist/orchestrator/host-operations.js +78 -0
- package/dist/orchestrator/lifecycle-operations.js +462 -0
- package/dist/orchestrator/migration-operations.js +44 -0
- package/dist/orchestrator/multi-agent-operations.js +362 -0
- package/dist/orchestrator/report.js +369 -0
- package/dist/orchestrator/topology-operations.js +84 -0
- package/dist/orchestrator.js +874 -0
- package/dist/pipeline-contract.js +92 -0
- package/dist/pipeline-runner.js +285 -0
- package/dist/reclamation.js +882 -0
- package/dist/result-normalize.js +194 -0
- package/dist/run-export.js +64 -0
- package/dist/run-registry.js +1347 -0
- package/dist/run-state-schema.js +67 -0
- package/dist/sandbox-profile.js +471 -0
- package/dist/scheduler.js +266 -0
- package/dist/scheduling.js +184 -0
- package/dist/schema-validate.js +98 -0
- package/dist/state-explosion.js +1213 -0
- package/dist/state-migrations.js +463 -0
- package/dist/state-node.js +301 -0
- package/dist/state.js +308 -0
- package/dist/telemetry-attestation.js +156 -0
- package/dist/telemetry-ledger.js +145 -0
- package/dist/topology.js +527 -0
- package/dist/triggers.js +159 -0
- package/dist/trust-audit.js +475 -0
- package/dist/types/blackboard.js +2 -0
- package/dist/types/boundary.js +29 -0
- package/dist/types/candidate.js +2 -0
- package/dist/types/collaboration.js +2 -0
- package/dist/types/core.js +2 -0
- package/dist/types/drive.js +10 -0
- package/dist/types/error-feedback.js +2 -0
- package/dist/types/evidence-reasoning.js +2 -0
- package/dist/types/execution-backend.js +2 -0
- package/dist/types/multi-agent.js +2 -0
- package/dist/types/observability.js +2 -0
- package/dist/types/pipeline.js +2 -0
- package/dist/types/reclamation.js +8 -0
- package/dist/types/result.js +2 -0
- package/dist/types/run-registry.js +2 -0
- package/dist/types/run.js +2 -0
- package/dist/types/sandbox.js +2 -0
- package/dist/types/schedule.js +2 -0
- package/dist/types/state-node.js +2 -0
- package/dist/types/topology.js +2 -0
- package/dist/types/trust.js +2 -0
- package/dist/types/workbench.js +2 -0
- package/dist/types/worker.js +2 -0
- package/dist/types/workflow-app.js +2 -0
- package/dist/types.js +43 -0
- package/dist/verifier-registry.js +46 -0
- package/dist/verifier.js +78 -0
- package/dist/version.js +8 -0
- package/dist/workbench-host.js +172 -0
- package/dist/workbench.js +190 -0
- package/dist/worker-isolation.js +1028 -0
- package/dist/workflow-api.js +98 -0
- package/dist/workflow-app-framework.js +626 -0
- package/docs/agent-delegation-drive.7.md +190 -0
- package/docs/agent-framework.md +176 -0
- package/docs/candidate-scoring.7.md +106 -0
- package/docs/canonical-workflow-apps.7.md +137 -0
- package/docs/capability-topology-registry.7.md +168 -0
- package/docs/cli-mcp-parity.7.md +373 -0
- package/docs/contract-migration-tooling.7.md +123 -0
- package/docs/control-plane-scheduling.7.md +110 -0
- package/docs/coordinator-blackboard.7.md +183 -0
- package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
- package/docs/dogfood-one-real-repo.7.md +168 -0
- package/docs/durable-state-and-locking.7.md +107 -0
- package/docs/end-to-end-golden-path.7.md +117 -0
- package/docs/error-feedback.7.md +153 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
- package/docs/execution-backends.7.md +300 -0
- package/docs/getting-started.md +99 -0
- package/docs/index.md +41 -0
- package/docs/mcp-app-surface.7.md +235 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
- package/docs/multi-agent-eval-replay-harness.7.md +302 -0
- package/docs/multi-agent-operator-ux.7.md +314 -0
- package/docs/multi-agent-runtime-core.7.md +231 -0
- package/docs/multi-agent-topologies.7.md +103 -0
- package/docs/multi-agent-trust-policy-audit.7.md +154 -0
- package/docs/node-snapshot-diff-replay.7.md +135 -0
- package/docs/observability-cost-accounting.7.md +194 -0
- package/docs/operator-ux.7.md +180 -0
- package/docs/pipeline-runner.7.md +136 -0
- package/docs/project-index.md +261 -0
- package/docs/real-execution-backends.7.md +142 -0
- package/docs/release-and-migration.7.md +280 -0
- package/docs/release-tooling.7.md +159 -0
- package/docs/routines.md +48 -0
- package/docs/run-registry-control-plane.7.md +312 -0
- package/docs/run-retention-reclamation.7.md +191 -0
- package/docs/sandbox-profiles.7.md +137 -0
- package/docs/scheduled-tasks.md +80 -0
- package/docs/security-trust-hardening.7.md +117 -0
- package/docs/state-explosion-management.7.md +264 -0
- package/docs/state-node.7.md +96 -0
- package/docs/team-collaboration.7.md +207 -0
- package/docs/unix-principles.md +192 -0
- package/docs/verifier-gated-commit.7.md +140 -0
- package/docs/web-desktop-workbench.7.md +215 -0
- package/docs/worker-isolation.7.md +167 -0
- package/docs/workflow-app-framework.7.md +274 -0
- package/manifest/README.md +43 -0
- package/manifest/plugin.manifest.json +316 -0
- package/manifest/pricing.policy.json +14 -0
- package/package.json +79 -0
- package/scripts/agents/claude-p-agent.js +104 -0
- package/scripts/agents/claude-p-agent.sh +9 -0
- package/scripts/agents/cw-attest-keygen.js +55 -0
- package/scripts/agents/cw-attest-wrap.js +143 -0
- package/scripts/block-unapproved-tag.sh +39 -0
- package/scripts/bump-version.js +249 -0
- package/scripts/canonical-apps.js +171 -0
- package/scripts/cw.js +4 -0
- package/scripts/dist-drift-check.js +79 -0
- package/scripts/dogfood-architecture-review.js +237 -0
- package/scripts/dogfood-release.js +624 -0
- package/scripts/forward-ref-docs.js +73 -0
- package/scripts/gen-manifests.js +232 -0
- package/scripts/golden-path.js +300 -0
- package/scripts/mcp-server.js +4 -0
- package/scripts/new-feature.js +121 -0
- package/scripts/parity-check.js +213 -0
- package/scripts/release-check.js +118 -0
- package/scripts/release-flow.js +272 -0
- package/scripts/release-gate.sh +85 -0
- package/scripts/sync-project-index.js +387 -0
- package/scripts/validate-run-state-schema.js +126 -0
- package/scripts/verify-container-selfref.js +64 -0
- package/scripts/version-sync-check.js +237 -0
- package/skills/cool-workflow/SKILL.md +162 -0
- package/skills/cool-workflow/references/commands.md +282 -0
- package/tsconfig.json +16 -0
- package/ui/workbench/app.css +76 -0
- package/ui/workbench/app.js +159 -0
- package/ui/workbench/index.html +32 -0
- package/workflows/architecture-review.workflow.js +84 -0
- package/workflows/research-synthesis.workflow.js +47 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Team Collaboration core (v0.1.32) — the human-decision layer.
|
|
3
|
+
//
|
|
4
|
+
// BSD discipline applied here:
|
|
5
|
+
// - IDENTITY IS ATTESTED, NOT AUTHENTICATED. `normalizeActor` records WHO acted
|
|
6
|
+
// from host/operator provenance; an absent identity becomes the explicit
|
|
7
|
+
// `unattributed` actor, never a fabricated one. CW is not an auth server.
|
|
8
|
+
// - REVIEW GATES STACK ON THE VERIFIER GATE. `reviewGateErrors` returns extra
|
|
9
|
+
// StateNodeErrors for `resolveCommitGate`/`selectCandidate` to APPEND — policy
|
|
10
|
+
// layered on top of the verifier mechanism, never replacing it.
|
|
11
|
+
// - APPEND-ONLY LOG; NEVER MUTATE THE PAST. record* only push; a correction is a
|
|
12
|
+
// NEW record carrying `supersedes`. The approved artifact is never edited; the
|
|
13
|
+
// review link is provenance, not a field overwrite.
|
|
14
|
+
// - FAIL CLOSED ON AUTHORITY AND QUORUM. `deriveReviewState` counts only
|
|
15
|
+
// distinct, attested, authorized, non-self approvals; anything short is
|
|
16
|
+
// pending/blocked/rejected/unattributed — never auto-passed.
|
|
17
|
+
// - POLICY AS DATA. The ReviewGatePolicy lives on the run (or is injected),
|
|
18
|
+
// out of the kernel; default (absent / requiredApprovals 0) keeps pre-v0.1.32
|
|
19
|
+
// behavior unchanged.
|
|
20
|
+
// - COLLABORATION IS STATE, NOT CHAT. Every record attaches to a durable target
|
|
21
|
+
// and is derived deterministically — no hidden dashboard.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.UNATTRIBUTED_ACTOR = exports.COLLABORATION_SCHEMA_VERSION = void 0;
|
|
24
|
+
exports.ensureCollaborationState = ensureCollaborationState;
|
|
25
|
+
exports.normalizeActor = normalizeActor;
|
|
26
|
+
exports.recordApproval = recordApproval;
|
|
27
|
+
exports.recordComment = recordComment;
|
|
28
|
+
exports.recordHandoff = recordHandoff;
|
|
29
|
+
exports.setReviewPolicy = setReviewPolicy;
|
|
30
|
+
exports.resolveReviewPolicy = resolveReviewPolicy;
|
|
31
|
+
exports.deriveReviewState = deriveReviewState;
|
|
32
|
+
exports.reviewGateErrors = reviewGateErrors;
|
|
33
|
+
exports.commitReviewProvenance = commitReviewProvenance;
|
|
34
|
+
exports.selfActorIdsForCandidate = selfActorIdsForCandidate;
|
|
35
|
+
exports.buildReviewStatusReport = buildReviewStatusReport;
|
|
36
|
+
exports.listComments = listComments;
|
|
37
|
+
exports.deriveOwner = deriveOwner;
|
|
38
|
+
exports.formatReviewStatus = formatReviewStatus;
|
|
39
|
+
exports.formatCommentList = formatCommentList;
|
|
40
|
+
const trust_audit_1 = require("./trust-audit");
|
|
41
|
+
const state_1 = require("./state");
|
|
42
|
+
exports.COLLABORATION_SCHEMA_VERSION = 1;
|
|
43
|
+
/** The single, honest stand-in for an absent identity. */
|
|
44
|
+
exports.UNATTRIBUTED_ACTOR = {
|
|
45
|
+
kind: "unattributed",
|
|
46
|
+
id: "unattributed",
|
|
47
|
+
attestation: "unattributed",
|
|
48
|
+
attested: false,
|
|
49
|
+
source: "runtime-derived"
|
|
50
|
+
};
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// State + actor normalization
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
function ensureCollaborationState(run) {
|
|
55
|
+
if (!run.collaboration) {
|
|
56
|
+
run.collaboration = { schemaVersion: exports.COLLABORATION_SCHEMA_VERSION, approvals: [], comments: [], handoffs: [] };
|
|
57
|
+
}
|
|
58
|
+
const state = run.collaboration;
|
|
59
|
+
if (!Array.isArray(state.approvals))
|
|
60
|
+
state.approvals = [];
|
|
61
|
+
if (!Array.isArray(state.comments))
|
|
62
|
+
state.comments = [];
|
|
63
|
+
if (!Array.isArray(state.handoffs))
|
|
64
|
+
state.handoffs = [];
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
const ACTOR_KINDS = ["operator", "worker", "role", "membership", "group", "host", "service", "unattributed"];
|
|
68
|
+
/** Build a host-attested (never authenticated) actor. Absent id => unattributed. */
|
|
69
|
+
function normalizeActor(input) {
|
|
70
|
+
const id = trimmed(input?.actor);
|
|
71
|
+
if (!id)
|
|
72
|
+
return { ...exports.UNATTRIBUTED_ACTOR };
|
|
73
|
+
const roleId = trimmed(input?.roleId) || trimmed(input?.role);
|
|
74
|
+
const kind = normalizeActorKind(input?.actorKind, roleId);
|
|
75
|
+
const attestation = input?.attestation
|
|
76
|
+
? input.attestation
|
|
77
|
+
: input?.attested
|
|
78
|
+
? "host-attested"
|
|
79
|
+
: "operator-recorded";
|
|
80
|
+
const attested = attestation === "host-attested";
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
id,
|
|
84
|
+
displayName: trimmed(input?.displayName) || undefined,
|
|
85
|
+
attestation,
|
|
86
|
+
attested,
|
|
87
|
+
roleId: roleId || undefined,
|
|
88
|
+
source: sourceForAttestation(attestation)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function normalizeActorKind(raw, roleId) {
|
|
92
|
+
const value = trimmed(raw);
|
|
93
|
+
if (value && ACTOR_KINDS.includes(value))
|
|
94
|
+
return value;
|
|
95
|
+
if (roleId)
|
|
96
|
+
return "role";
|
|
97
|
+
return "operator";
|
|
98
|
+
}
|
|
99
|
+
function sourceForAttestation(attestation) {
|
|
100
|
+
if (attestation === "host-attested")
|
|
101
|
+
return "host-attested";
|
|
102
|
+
if (attestation === "operator-recorded")
|
|
103
|
+
return "operator-recorded";
|
|
104
|
+
return "runtime-derived";
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Append-only record writers
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
function recordApproval(run, input, options = {}) {
|
|
110
|
+
const state = ensureCollaborationState(run);
|
|
111
|
+
const actor = normalizeActor(input);
|
|
112
|
+
const target = normalizeTarget(input.target);
|
|
113
|
+
const decision = input.decision === "reject" ? "reject" : "approve";
|
|
114
|
+
const audit = (0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
115
|
+
kind: decision === "approve" ? "collaboration.approval" : "collaboration.rejection",
|
|
116
|
+
decision: decision === "approve" ? "accepted" : "rejected",
|
|
117
|
+
source: actor.source,
|
|
118
|
+
actor: actor.id,
|
|
119
|
+
...auditTargetFields(target),
|
|
120
|
+
agentRoleId: actor.roleId,
|
|
121
|
+
metadata: compact({
|
|
122
|
+
decision,
|
|
123
|
+
rationale: input.rationale,
|
|
124
|
+
roleId: actor.roleId,
|
|
125
|
+
attestation: actor.attestation,
|
|
126
|
+
targetKind: target.kind,
|
|
127
|
+
supersedes: input.supersedes
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
const record = compact({
|
|
131
|
+
schemaVersion: exports.COLLABORATION_SCHEMA_VERSION,
|
|
132
|
+
id: createCollabId(run, decision === "approve" ? "approval" : "rejection", state.approvals.length),
|
|
133
|
+
runId: run.id,
|
|
134
|
+
createdAt: new Date().toISOString(),
|
|
135
|
+
actor,
|
|
136
|
+
decision,
|
|
137
|
+
target,
|
|
138
|
+
rationale: trimmed(input.rationale) || undefined,
|
|
139
|
+
roleId: actor.roleId,
|
|
140
|
+
supersedes: trimmed(input.supersedes) || undefined,
|
|
141
|
+
auditEventIds: [audit.id],
|
|
142
|
+
metadata: undefined
|
|
143
|
+
});
|
|
144
|
+
state.approvals.push(record);
|
|
145
|
+
persist(run, options);
|
|
146
|
+
return record;
|
|
147
|
+
}
|
|
148
|
+
function recordComment(run, input, options = {}) {
|
|
149
|
+
const state = ensureCollaborationState(run);
|
|
150
|
+
const actor = normalizeActor(input);
|
|
151
|
+
const target = normalizeTarget(input.target);
|
|
152
|
+
const body = trimmed(input.body);
|
|
153
|
+
if (!body)
|
|
154
|
+
throw new Error("Comment body is required");
|
|
155
|
+
const threadId = trimmed(input.threadId) || `${target.kind}:${target.id}`;
|
|
156
|
+
const audit = (0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
157
|
+
kind: "collaboration.comment",
|
|
158
|
+
decision: "recorded",
|
|
159
|
+
source: actor.source,
|
|
160
|
+
actor: actor.id,
|
|
161
|
+
...auditTargetFields(target),
|
|
162
|
+
agentRoleId: actor.roleId,
|
|
163
|
+
metadata: compact({ threadId, parentId: input.parentId, targetKind: target.kind })
|
|
164
|
+
});
|
|
165
|
+
const record = compact({
|
|
166
|
+
schemaVersion: exports.COLLABORATION_SCHEMA_VERSION,
|
|
167
|
+
id: createCollabId(run, "comment", state.comments.length),
|
|
168
|
+
runId: run.id,
|
|
169
|
+
createdAt: new Date().toISOString(),
|
|
170
|
+
actor,
|
|
171
|
+
target,
|
|
172
|
+
body,
|
|
173
|
+
threadId,
|
|
174
|
+
parentId: trimmed(input.parentId) || undefined,
|
|
175
|
+
auditEventIds: [audit.id]
|
|
176
|
+
});
|
|
177
|
+
state.comments.push(record);
|
|
178
|
+
persist(run, options);
|
|
179
|
+
return record;
|
|
180
|
+
}
|
|
181
|
+
function recordHandoff(run, input, options = {}) {
|
|
182
|
+
const state = ensureCollaborationState(run);
|
|
183
|
+
const recorder = normalizeActor(input);
|
|
184
|
+
const fromActor = input.fromActor
|
|
185
|
+
? normalizeActor({ actor: input.fromActor, actorKind: input.fromActorKind, role: input.fromRole, attested: input.attested })
|
|
186
|
+
: recorder;
|
|
187
|
+
const toActor = normalizeActor({
|
|
188
|
+
actor: input.toActor,
|
|
189
|
+
actorKind: input.toActorKind,
|
|
190
|
+
role: input.toRole,
|
|
191
|
+
displayName: input.toDisplayName,
|
|
192
|
+
attested: input.toAttested
|
|
193
|
+
});
|
|
194
|
+
if (toActor.kind === "unattributed")
|
|
195
|
+
throw new Error("Handoff requires a to-actor (--to)");
|
|
196
|
+
const target = normalizeTarget(input.target);
|
|
197
|
+
const reason = trimmed(input.reason) || "handoff";
|
|
198
|
+
const audit = (0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
199
|
+
kind: "collaboration.handoff",
|
|
200
|
+
decision: "recorded",
|
|
201
|
+
source: recorder.source,
|
|
202
|
+
actor: recorder.id,
|
|
203
|
+
...auditTargetFields(target),
|
|
204
|
+
metadata: compact({ from: fromActor.id, to: toActor.id, reason, targetKind: target.kind })
|
|
205
|
+
});
|
|
206
|
+
const record = compact({
|
|
207
|
+
schemaVersion: exports.COLLABORATION_SCHEMA_VERSION,
|
|
208
|
+
id: createCollabId(run, "handoff", state.handoffs.length),
|
|
209
|
+
runId: run.id,
|
|
210
|
+
createdAt: new Date().toISOString(),
|
|
211
|
+
actor: recorder,
|
|
212
|
+
fromActor,
|
|
213
|
+
toActor,
|
|
214
|
+
target,
|
|
215
|
+
reason,
|
|
216
|
+
auditEventIds: [audit.id]
|
|
217
|
+
});
|
|
218
|
+
state.handoffs.push(record);
|
|
219
|
+
persist(run, options);
|
|
220
|
+
return record;
|
|
221
|
+
}
|
|
222
|
+
function setReviewPolicy(run, input, options = {}) {
|
|
223
|
+
const state = ensureCollaborationState(run);
|
|
224
|
+
const policy = {
|
|
225
|
+
schemaVersion: exports.COLLABORATION_SCHEMA_VERSION,
|
|
226
|
+
id: state.policy?.id || createCollabId(run, "policy", 0),
|
|
227
|
+
requiredApprovals: Math.max(0, Math.floor(toNumber(input.requiredApprovals, state.policy?.requiredApprovals ?? 0))),
|
|
228
|
+
authorizedRoles: toStringList(input.authorizedRoles, state.policy?.authorizedRoles ?? ["*"]),
|
|
229
|
+
allowSelfApproval: input.allowSelfApproval ?? state.policy?.allowSelfApproval ?? false,
|
|
230
|
+
requireAttestedActor: input.requireAttestedActor ?? state.policy?.requireAttestedActor ?? false,
|
|
231
|
+
appliesTo: toTargetKindList(input.appliesTo, state.policy?.appliesTo ?? ["commit"]),
|
|
232
|
+
updatedAt: new Date().toISOString()
|
|
233
|
+
};
|
|
234
|
+
state.policy = policy;
|
|
235
|
+
(0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
236
|
+
kind: "collaboration.review-policy",
|
|
237
|
+
decision: "recorded",
|
|
238
|
+
source: "operator-recorded",
|
|
239
|
+
metadata: compact({
|
|
240
|
+
policyId: policy.id,
|
|
241
|
+
requiredApprovals: policy.requiredApprovals,
|
|
242
|
+
authorizedRoles: policy.authorizedRoles,
|
|
243
|
+
allowSelfApproval: policy.allowSelfApproval,
|
|
244
|
+
requireAttestedActor: policy.requireAttestedActor,
|
|
245
|
+
appliesTo: policy.appliesTo
|
|
246
|
+
})
|
|
247
|
+
});
|
|
248
|
+
persist(run, options);
|
|
249
|
+
return policy;
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Deterministic review-state derivation (the fail-closed heart)
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
function resolveReviewPolicy(run, policy) {
|
|
255
|
+
return policy || run.collaboration?.policy || undefined;
|
|
256
|
+
}
|
|
257
|
+
/** Pure projection: derive a target's review state from append-only records +
|
|
258
|
+
* policy. Deterministic over a fixed run snapshot (no wall-clock). */
|
|
259
|
+
function deriveReviewState(run, target, options = {}) {
|
|
260
|
+
const normalized = normalizeTarget(target);
|
|
261
|
+
const policy = resolveReviewPolicy(run, options.policy);
|
|
262
|
+
const related = (options.relatedTargets && options.relatedTargets.length ? options.relatedTargets : [normalized]).map(normalizeTarget);
|
|
263
|
+
const selfIds = new Set((options.selfActorIds || []).filter(Boolean));
|
|
264
|
+
const approvals = (run.collaboration?.approvals || []).filter((record) => matchesAnyTarget(record.target, related));
|
|
265
|
+
// git-style supersession: a record named by any `supersedes` is retired.
|
|
266
|
+
const supersededIds = new Set(approvals.map((record) => record.supersedes).filter((id) => Boolean(id)));
|
|
267
|
+
const gated = Boolean(policy && policy.requiredApprovals > 0 && policy.appliesTo.includes(normalized.kind));
|
|
268
|
+
const required = gated ? policy.requiredApprovals : 0;
|
|
269
|
+
const counted = [];
|
|
270
|
+
const countedActorIds = new Set();
|
|
271
|
+
const rejections = [];
|
|
272
|
+
const disqualified = [];
|
|
273
|
+
for (const record of [...approvals].sort(compareByCreated)) {
|
|
274
|
+
if (supersededIds.has(record.id)) {
|
|
275
|
+
disqualified.push({ approvalId: record.id, actorId: record.actor.id, reason: "superseded" });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const reason = disqualify(record, policy, selfIds);
|
|
279
|
+
if (record.decision === "reject") {
|
|
280
|
+
// A reject from an authorized, attested actor is a blocking veto.
|
|
281
|
+
if (!reason || reason === "self-approval")
|
|
282
|
+
rejections.push(record);
|
|
283
|
+
else
|
|
284
|
+
disqualified.push({ approvalId: record.id, actorId: record.actor.id, reason });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (reason) {
|
|
288
|
+
disqualified.push({ approvalId: record.id, actorId: record.actor.id, reason });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (!countedActorIds.has(record.actor.id)) {
|
|
292
|
+
countedActorIds.add(record.actor.id);
|
|
293
|
+
counted.push(record);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const recordedApprovals = countedActorIds.size;
|
|
297
|
+
const status = deriveStatus(gated, required, recordedApprovals, rejections.length, disqualified);
|
|
298
|
+
const approvers = [...countedActorIds].sort();
|
|
299
|
+
const missing = buildMissing(status, gated, required, recordedApprovals, policy, rejections, disqualified);
|
|
300
|
+
return {
|
|
301
|
+
schemaVersion: exports.COLLABORATION_SCHEMA_VERSION,
|
|
302
|
+
runId: run.id,
|
|
303
|
+
target: normalized,
|
|
304
|
+
status,
|
|
305
|
+
gated,
|
|
306
|
+
policyId: policy?.id,
|
|
307
|
+
requiredApprovals: required,
|
|
308
|
+
recordedApprovals,
|
|
309
|
+
approvers,
|
|
310
|
+
approvals: counted,
|
|
311
|
+
rejections,
|
|
312
|
+
disqualified,
|
|
313
|
+
missing
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function disqualify(record, policy, selfIds) {
|
|
317
|
+
const actor = record.actor;
|
|
318
|
+
if (actor.kind === "unattributed")
|
|
319
|
+
return "unattributed";
|
|
320
|
+
if (policy?.requireAttestedActor && !actor.attested)
|
|
321
|
+
return "unattributed";
|
|
322
|
+
if (policy && !roleAuthorized(actor.roleId, policy.authorizedRoles))
|
|
323
|
+
return "unauthorized-role";
|
|
324
|
+
if (policy && !policy.allowSelfApproval && selfIds.has(actor.id))
|
|
325
|
+
return "self-approval";
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
function roleAuthorized(roleId, authorizedRoles) {
|
|
329
|
+
if (authorizedRoles.includes("*"))
|
|
330
|
+
return true;
|
|
331
|
+
if (!roleId)
|
|
332
|
+
return false;
|
|
333
|
+
return authorizedRoles.includes(roleId);
|
|
334
|
+
}
|
|
335
|
+
function deriveStatus(gated, required, recorded, rejectionCount, disqualified) {
|
|
336
|
+
if (!gated)
|
|
337
|
+
return "approved";
|
|
338
|
+
if (rejectionCount > 0)
|
|
339
|
+
return "rejected";
|
|
340
|
+
if (recorded >= required)
|
|
341
|
+
return "approved";
|
|
342
|
+
if (recorded === 0 && disqualified.length > 0) {
|
|
343
|
+
const blocking = disqualified.filter((entry) => entry.reason !== "superseded");
|
|
344
|
+
if (blocking.length > 0 && blocking.every((entry) => entry.reason === "unattributed"))
|
|
345
|
+
return "unattributed";
|
|
346
|
+
if (blocking.length > 0)
|
|
347
|
+
return "blocked";
|
|
348
|
+
}
|
|
349
|
+
return "pending";
|
|
350
|
+
}
|
|
351
|
+
function buildMissing(status, gated, required, recorded, policy, rejections, disqualified) {
|
|
352
|
+
if (!gated || status === "approved")
|
|
353
|
+
return [];
|
|
354
|
+
const missing = [];
|
|
355
|
+
if (status === "rejected") {
|
|
356
|
+
for (const record of rejections)
|
|
357
|
+
missing.push(`rejected by ${record.actor.id}${record.rationale ? ` (${record.rationale})` : ""}`);
|
|
358
|
+
return missing;
|
|
359
|
+
}
|
|
360
|
+
const roles = policy?.authorizedRoles?.length ? policy.authorizedRoles.join(", ") : "*";
|
|
361
|
+
missing.push(`${required - recorded} more approval(s) from authorized role(s) [${roles}] required (have ${recorded}/${required})`);
|
|
362
|
+
const selfCount = disqualified.filter((entry) => entry.reason === "self-approval").length;
|
|
363
|
+
const unattributedCount = disqualified.filter((entry) => entry.reason === "unattributed").length;
|
|
364
|
+
const unauthorizedCount = disqualified.filter((entry) => entry.reason === "unauthorized-role").length;
|
|
365
|
+
if (selfCount)
|
|
366
|
+
missing.push(`${selfCount} self-approval(s) ignored (policy forbids self-approval)`);
|
|
367
|
+
if (unattributedCount)
|
|
368
|
+
missing.push(`${unattributedCount} unattributed approval(s) ignored`);
|
|
369
|
+
if (unauthorizedCount)
|
|
370
|
+
missing.push(`${unauthorizedCount} approval(s) from unauthorized role(s) ignored`);
|
|
371
|
+
return missing;
|
|
372
|
+
}
|
|
373
|
+
/** The StateNodeErrors a review gate contributes. Empty when the target is not
|
|
374
|
+
* gated or the gate is satisfied — so it can only ADD constraints, never remove
|
|
375
|
+
* the verifier's. */
|
|
376
|
+
function reviewGateErrors(run, input) {
|
|
377
|
+
const policy = resolveReviewPolicy(run, input.policy);
|
|
378
|
+
if (!policy || policy.requiredApprovals <= 0 || !policy.appliesTo.includes(input.targetKind))
|
|
379
|
+
return [];
|
|
380
|
+
const target = gateTarget(input);
|
|
381
|
+
const related = gateRelatedTargets(input);
|
|
382
|
+
const state = deriveReviewState(run, target, { policy, relatedTargets: related, selfActorIds: input.selfActorIds });
|
|
383
|
+
if (state.status === "approved")
|
|
384
|
+
return [];
|
|
385
|
+
return [
|
|
386
|
+
{
|
|
387
|
+
code: "review-gate-missing-approvals",
|
|
388
|
+
message: `Review gate blocked (${state.status}): ${state.missing.join("; ")}`,
|
|
389
|
+
at: new Date().toISOString(),
|
|
390
|
+
retryable: false,
|
|
391
|
+
details: {
|
|
392
|
+
reviewStatus: state.status,
|
|
393
|
+
requiredApprovals: state.requiredApprovals,
|
|
394
|
+
recordedApprovals: state.recordedApprovals,
|
|
395
|
+
approvers: state.approvers,
|
|
396
|
+
missing: state.missing,
|
|
397
|
+
policyId: state.policyId,
|
|
398
|
+
targetKind: input.targetKind
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
/** When a gated commit passes, stamp who approved the very artifact that shipped. */
|
|
404
|
+
function commitReviewProvenance(run, input) {
|
|
405
|
+
const policy = resolveReviewPolicy(run, input.policy);
|
|
406
|
+
if (!policy || policy.requiredApprovals <= 0 || !policy.appliesTo.includes(input.targetKind))
|
|
407
|
+
return undefined;
|
|
408
|
+
const target = gateTarget(input);
|
|
409
|
+
const state = deriveReviewState(run, target, {
|
|
410
|
+
policy,
|
|
411
|
+
relatedTargets: gateRelatedTargets(input),
|
|
412
|
+
selfActorIds: input.selfActorIds
|
|
413
|
+
});
|
|
414
|
+
if (state.status !== "approved")
|
|
415
|
+
return undefined;
|
|
416
|
+
return {
|
|
417
|
+
policyId: policy.id,
|
|
418
|
+
requiredApprovals: state.requiredApprovals,
|
|
419
|
+
recordedApprovals: state.recordedApprovals,
|
|
420
|
+
approvers: state.approvers,
|
|
421
|
+
approvalIds: state.approvals.map((record) => record.id).sort(),
|
|
422
|
+
target
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function gateTarget(input) {
|
|
426
|
+
if (input.targetKind === "commit")
|
|
427
|
+
return { kind: "commit", id: input.commitId || "(pending)" };
|
|
428
|
+
if (input.targetKind === "selection")
|
|
429
|
+
return { kind: "selection", id: input.selectionId || "(pending)" };
|
|
430
|
+
if (input.targetKind === "candidate")
|
|
431
|
+
return { kind: "candidate", id: input.candidateId || "(pending)" };
|
|
432
|
+
if (input.targetKind === "node")
|
|
433
|
+
return { kind: "node", id: input.nodeId || "(pending)" };
|
|
434
|
+
if (input.targetKind === "task")
|
|
435
|
+
return { kind: "task", id: input.taskId || "(pending)" };
|
|
436
|
+
return { kind: "run", id: run_id_or_pending(input) };
|
|
437
|
+
}
|
|
438
|
+
function run_id_or_pending(input) {
|
|
439
|
+
return input.commitId || input.candidateId || input.selectionId || "(pending)";
|
|
440
|
+
}
|
|
441
|
+
/** A commit/selection counts approvals on ITSELF and its underlying
|
|
442
|
+
* candidate/selection — you approve the candidate, the commit honors it. */
|
|
443
|
+
function gateRelatedTargets(input) {
|
|
444
|
+
const related = [];
|
|
445
|
+
if (input.commitId)
|
|
446
|
+
related.push({ kind: "commit", id: input.commitId });
|
|
447
|
+
if (input.selectionId)
|
|
448
|
+
related.push({ kind: "selection", id: input.selectionId });
|
|
449
|
+
if (input.candidateId)
|
|
450
|
+
related.push({ kind: "candidate", id: input.candidateId });
|
|
451
|
+
if (input.nodeId)
|
|
452
|
+
related.push({ kind: "node", id: input.nodeId });
|
|
453
|
+
if (input.taskId)
|
|
454
|
+
related.push({ kind: "task", id: input.taskId });
|
|
455
|
+
if (!related.length)
|
|
456
|
+
related.push(gateTarget(input));
|
|
457
|
+
return related;
|
|
458
|
+
}
|
|
459
|
+
/** Self ids for a candidate/selection target: its producing worker + selector. */
|
|
460
|
+
function selfActorIdsForCandidate(run, candidateId, selectionId) {
|
|
461
|
+
const ids = new Set();
|
|
462
|
+
const candidate = candidateId ? (run.candidates || []).find((entry) => entry.id === candidateId) : undefined;
|
|
463
|
+
if (candidate?.workerId)
|
|
464
|
+
ids.add(candidate.workerId);
|
|
465
|
+
const selections = (run.candidateSelections || []).filter((selection) => (selectionId && selection.id === selectionId) || (candidateId && selection.candidateId === candidateId));
|
|
466
|
+
for (const selection of selections)
|
|
467
|
+
if (selection.selectedBy)
|
|
468
|
+
ids.add(selection.selectedBy);
|
|
469
|
+
return [...ids];
|
|
470
|
+
}
|
|
471
|
+
function buildReviewStatusReport(run, options) {
|
|
472
|
+
ensureCollaborationState(run);
|
|
473
|
+
const policy = run.collaboration?.policy;
|
|
474
|
+
const approvals = run.collaboration?.approvals || [];
|
|
475
|
+
const comments = run.collaboration?.comments || [];
|
|
476
|
+
const handoffs = run.collaboration?.handoffs || [];
|
|
477
|
+
const targets = options.target ? [normalizeTarget(options.target)] : distinctTargets(run);
|
|
478
|
+
const reviewStates = targets.map((target) => deriveReviewState(run, target, {
|
|
479
|
+
policy,
|
|
480
|
+
relatedTargets: relatedTargetsFor(run, target),
|
|
481
|
+
selfActorIds: selfActorIdsForTarget(run, target)
|
|
482
|
+
}));
|
|
483
|
+
const owner = deriveOwner(run);
|
|
484
|
+
const timeline = buildTimeline(run);
|
|
485
|
+
return {
|
|
486
|
+
schemaVersion: exports.COLLABORATION_SCHEMA_VERSION,
|
|
487
|
+
surface: "collaboration",
|
|
488
|
+
runId: run.id,
|
|
489
|
+
generatedAt: options.now,
|
|
490
|
+
policy,
|
|
491
|
+
owner,
|
|
492
|
+
targets: reviewStates,
|
|
493
|
+
counts: {
|
|
494
|
+
approvals: approvals.filter((record) => record.decision === "approve").length,
|
|
495
|
+
rejections: approvals.filter((record) => record.decision === "reject").length,
|
|
496
|
+
comments: comments.length,
|
|
497
|
+
handoffs: handoffs.length
|
|
498
|
+
},
|
|
499
|
+
timeline,
|
|
500
|
+
nextActions: buildNextActions(run, reviewStates, policy)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function listComments(run, target) {
|
|
504
|
+
const comments = run.collaboration?.comments || [];
|
|
505
|
+
const filtered = target ? comments.filter((record) => sameTarget(record.target, normalizeTarget(target))) : comments;
|
|
506
|
+
return [...filtered].sort(compareByCreated);
|
|
507
|
+
}
|
|
508
|
+
function deriveOwner(run) {
|
|
509
|
+
const handoffs = [...(run.collaboration?.handoffs || [])]
|
|
510
|
+
.filter((record) => record.target.kind === "run" || record.target.kind === "task")
|
|
511
|
+
.sort(compareByCreated);
|
|
512
|
+
return handoffs.length ? handoffs[handoffs.length - 1].toActor : undefined;
|
|
513
|
+
}
|
|
514
|
+
function buildTimeline(run) {
|
|
515
|
+
const entries = [];
|
|
516
|
+
for (const record of run.collaboration?.approvals || []) {
|
|
517
|
+
entries.push({
|
|
518
|
+
kind: "approval",
|
|
519
|
+
id: record.id,
|
|
520
|
+
createdAt: record.createdAt,
|
|
521
|
+
actor: record.actor,
|
|
522
|
+
target: record.target,
|
|
523
|
+
summary: `${record.decision === "approve" ? "approved" : "rejected"} ${record.target.kind} ${record.target.id}${record.rationale ? ` — ${record.rationale}` : ""}`
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
for (const record of run.collaboration?.comments || []) {
|
|
527
|
+
entries.push({
|
|
528
|
+
kind: "comment",
|
|
529
|
+
id: record.id,
|
|
530
|
+
createdAt: record.createdAt,
|
|
531
|
+
actor: record.actor,
|
|
532
|
+
target: record.target,
|
|
533
|
+
summary: `commented on ${record.target.kind} ${record.target.id}: ${truncate(record.body, 80)}`
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
for (const record of run.collaboration?.handoffs || []) {
|
|
537
|
+
entries.push({
|
|
538
|
+
kind: "handoff",
|
|
539
|
+
id: record.id,
|
|
540
|
+
createdAt: record.createdAt,
|
|
541
|
+
actor: record.actor,
|
|
542
|
+
target: record.target,
|
|
543
|
+
summary: `handed off ${record.target.kind} ${record.target.id}: ${record.fromActor.id} → ${record.toActor.id} (${record.reason})`
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (run.collaboration?.policy) {
|
|
547
|
+
const policy = run.collaboration.policy;
|
|
548
|
+
entries.push({
|
|
549
|
+
kind: "policy",
|
|
550
|
+
id: policy.id,
|
|
551
|
+
createdAt: policy.updatedAt,
|
|
552
|
+
actor: { ...exports.UNATTRIBUTED_ACTOR, kind: "operator", id: "operator", attestation: "operator-recorded", source: "operator-recorded" },
|
|
553
|
+
summary: `review policy: ${policy.requiredApprovals} approval(s) from [${policy.authorizedRoles.join(", ")}] for [${policy.appliesTo.join(", ")}]`
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return entries.sort(compareTimeline);
|
|
557
|
+
}
|
|
558
|
+
function buildNextActions(run, states, policy) {
|
|
559
|
+
const actions = [];
|
|
560
|
+
if (!policy) {
|
|
561
|
+
actions.push(`node scripts/cw.js review policy ${run.id} --requiredApprovals 1 --authorizedRoles reviewer --appliesTo commit`);
|
|
562
|
+
return actions;
|
|
563
|
+
}
|
|
564
|
+
for (const state of states) {
|
|
565
|
+
if (state.status === "pending" || state.status === "blocked" || state.status === "unattributed") {
|
|
566
|
+
actions.push(`node scripts/cw.js approve ${state.target.kind} ${run.id} ${state.target.id} --role <authorized-role> --actor <id> --attested`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (!actions.length)
|
|
570
|
+
actions.push(`node scripts/cw.js review status ${run.id} --json`);
|
|
571
|
+
return actions;
|
|
572
|
+
}
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Human formatters
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
function formatReviewStatus(report) {
|
|
577
|
+
const lines = [];
|
|
578
|
+
const policy = report.policy;
|
|
579
|
+
lines.push(`review ${report.runId} policy=${policy ? `${policy.requiredApprovals} from [${policy.authorizedRoles.join(",")}] on [${policy.appliesTo.join(",")}]` : "none"}`);
|
|
580
|
+
if (report.owner)
|
|
581
|
+
lines.push(` owner: ${report.owner.id} (${report.owner.attestation})`);
|
|
582
|
+
lines.push(` counts: approvals=${report.counts.approvals} rejections=${report.counts.rejections} comments=${report.counts.comments} handoffs=${report.counts.handoffs}`);
|
|
583
|
+
for (const state of report.targets) {
|
|
584
|
+
lines.push(` ${state.target.kind} ${state.target.id}: ${state.status}` +
|
|
585
|
+
(state.gated ? ` (${state.recordedApprovals}/${state.requiredApprovals}${state.approvers.length ? ` by ${state.approvers.join(",")}` : ""})` : " (not gated)"));
|
|
586
|
+
for (const note of state.missing)
|
|
587
|
+
lines.push(` - ${note}`);
|
|
588
|
+
}
|
|
589
|
+
if (report.timeline.length) {
|
|
590
|
+
lines.push(" timeline:");
|
|
591
|
+
for (const entry of report.timeline)
|
|
592
|
+
lines.push(` ${entry.createdAt} ${entry.actor.id} ${entry.summary}`);
|
|
593
|
+
}
|
|
594
|
+
return lines.join("\n");
|
|
595
|
+
}
|
|
596
|
+
function formatCommentList(comments) {
|
|
597
|
+
if (!comments.length)
|
|
598
|
+
return "no comments";
|
|
599
|
+
return comments
|
|
600
|
+
.map((record) => `${record.createdAt} ${record.actor.id} (${record.actor.attestation}) [${record.target.kind} ${record.target.id}] ${record.body}`)
|
|
601
|
+
.join("\n");
|
|
602
|
+
}
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// internals
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
function distinctTargets(run) {
|
|
607
|
+
const seen = new Map();
|
|
608
|
+
for (const record of run.collaboration?.approvals || [])
|
|
609
|
+
seen.set(targetKey(record.target), record.target);
|
|
610
|
+
for (const record of run.collaboration?.comments || [])
|
|
611
|
+
seen.set(targetKey(record.target), record.target);
|
|
612
|
+
for (const record of run.collaboration?.handoffs || [])
|
|
613
|
+
seen.set(targetKey(record.target), record.target);
|
|
614
|
+
return [...seen.values()].sort((left, right) => targetKey(left).localeCompare(targetKey(right)));
|
|
615
|
+
}
|
|
616
|
+
/** For a commit target, also count its candidate/selection approvals. */
|
|
617
|
+
function relatedTargetsFor(run, target) {
|
|
618
|
+
if (target.kind !== "commit")
|
|
619
|
+
return [target];
|
|
620
|
+
const commit = (run.commits || []).find((entry) => entry.id === target.id);
|
|
621
|
+
const related = [target];
|
|
622
|
+
if (commit?.selectionId)
|
|
623
|
+
related.push({ kind: "selection", id: commit.selectionId });
|
|
624
|
+
if (commit?.candidateId)
|
|
625
|
+
related.push({ kind: "candidate", id: commit.candidateId });
|
|
626
|
+
return related;
|
|
627
|
+
}
|
|
628
|
+
function selfActorIdsForTarget(run, target) {
|
|
629
|
+
if (target.kind === "candidate")
|
|
630
|
+
return selfActorIdsForCandidate(run, target.id);
|
|
631
|
+
if (target.kind === "selection") {
|
|
632
|
+
const selection = (run.candidateSelections || []).find((entry) => entry.id === target.id);
|
|
633
|
+
return selfActorIdsForCandidate(run, selection?.candidateId, target.id);
|
|
634
|
+
}
|
|
635
|
+
if (target.kind === "commit") {
|
|
636
|
+
const commit = (run.commits || []).find((entry) => entry.id === target.id);
|
|
637
|
+
return selfActorIdsForCandidate(run, commit?.candidateId, commit?.selectionId);
|
|
638
|
+
}
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
function normalizeTarget(target) {
|
|
642
|
+
const kind = target?.kind;
|
|
643
|
+
const id = trimmed(target?.id);
|
|
644
|
+
if (!kind || !id)
|
|
645
|
+
throw new Error("Collaboration target requires a kind and id");
|
|
646
|
+
if (!["run", "task", "candidate", "selection", "commit", "node"].includes(kind)) {
|
|
647
|
+
throw new Error(`Unknown collaboration target kind: ${kind}`);
|
|
648
|
+
}
|
|
649
|
+
return { kind, id };
|
|
650
|
+
}
|
|
651
|
+
function auditTargetFields(target) {
|
|
652
|
+
switch (target.kind) {
|
|
653
|
+
case "candidate":
|
|
654
|
+
return { candidateId: target.id };
|
|
655
|
+
case "selection":
|
|
656
|
+
return { selectionId: target.id };
|
|
657
|
+
case "commit":
|
|
658
|
+
return { commitId: target.id };
|
|
659
|
+
case "node":
|
|
660
|
+
return { nodeId: target.id };
|
|
661
|
+
case "task":
|
|
662
|
+
return { taskId: target.id };
|
|
663
|
+
default:
|
|
664
|
+
return {};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function matchesAnyTarget(target, related) {
|
|
668
|
+
return related.some((entry) => sameTarget(target, entry));
|
|
669
|
+
}
|
|
670
|
+
function sameTarget(left, right) {
|
|
671
|
+
return left.kind === right.kind && left.id === right.id;
|
|
672
|
+
}
|
|
673
|
+
function targetKey(target) {
|
|
674
|
+
return `${target.kind}:${target.id}`;
|
|
675
|
+
}
|
|
676
|
+
function createCollabId(run, kind, count) {
|
|
677
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
678
|
+
return `collab-${(0, state_1.safeFileName)(kind)}-${stamp}-${String(count + 1).padStart(4, "0")}`;
|
|
679
|
+
}
|
|
680
|
+
function persist(run, options) {
|
|
681
|
+
if (options.persist === false)
|
|
682
|
+
return;
|
|
683
|
+
(0, state_1.saveCheckpoint)(run);
|
|
684
|
+
}
|
|
685
|
+
function compareByCreated(left, right) {
|
|
686
|
+
return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
687
|
+
}
|
|
688
|
+
function compareTimeline(left, right) {
|
|
689
|
+
return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
690
|
+
}
|
|
691
|
+
function compact(value) {
|
|
692
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
693
|
+
}
|
|
694
|
+
function trimmed(value) {
|
|
695
|
+
if (value === undefined || value === null)
|
|
696
|
+
return "";
|
|
697
|
+
return String(value).trim();
|
|
698
|
+
}
|
|
699
|
+
function truncate(value, max) {
|
|
700
|
+
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
701
|
+
}
|
|
702
|
+
function toNumber(value, fallback) {
|
|
703
|
+
if (value === undefined || value === null || value === "" || value === true)
|
|
704
|
+
return fallback;
|
|
705
|
+
const parsed = Number(value);
|
|
706
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
707
|
+
}
|
|
708
|
+
function toStringList(value, fallback) {
|
|
709
|
+
if (value === undefined)
|
|
710
|
+
return fallback;
|
|
711
|
+
const list = Array.isArray(value) ? value : String(value).split(",");
|
|
712
|
+
const cleaned = list.map((entry) => String(entry).trim()).filter(Boolean);
|
|
713
|
+
return cleaned.length ? unique(cleaned) : fallback;
|
|
714
|
+
}
|
|
715
|
+
function toTargetKindList(value, fallback) {
|
|
716
|
+
if (value === undefined)
|
|
717
|
+
return fallback;
|
|
718
|
+
const list = Array.isArray(value) ? value : String(value).split(",");
|
|
719
|
+
const valid = ["run", "task", "candidate", "selection", "commit", "node"];
|
|
720
|
+
const cleaned = list
|
|
721
|
+
.map((entry) => String(entry).trim())
|
|
722
|
+
.filter((entry) => valid.includes(entry));
|
|
723
|
+
return cleaned.length ? unique(cleaned) : fallback;
|
|
724
|
+
}
|
|
725
|
+
function unique(values) {
|
|
726
|
+
return Array.from(new Set(values));
|
|
727
|
+
}
|