role-os 2.2.1 → 2.3.1
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/CHANGELOG.md +23 -0
- package/README.md +48 -13
- package/bin/roleos.mjs +11 -0
- package/package.json +2 -2
- package/src/artifacts.mjs +70 -0
- package/src/dispatch.mjs +8 -4
- package/src/knowledge/analyze-artifact-evidence.mjs +420 -0
- package/src/knowledge/attach-bundle-to-evidence.mjs +62 -0
- package/src/knowledge/attach-bundle-to-packet.mjs +42 -0
- package/src/knowledge/fallback-policy.mjs +79 -0
- package/src/knowledge/index.mjs +14 -0
- package/src/knowledge/render-knowledge-block.mjs +215 -0
- package/src/knowledge/resolve-overlay.mjs +66 -0
- package/src/knowledge/retrieve-for-dispatch.mjs +150 -0
- package/src/mission-run.mjs +114 -2
- package/src/mission.mjs +147 -0
- package/src/packs.mjs +37 -0
- package/src/route.mjs +51 -0
- package/src/run-cmd.mjs +4 -1
- package/src/run.mjs +42 -1
- package/src/swarm/build-gate.mjs +127 -0
- package/src/swarm/domain-detect.mjs +230 -0
- package/src/swarm/persist-bridge.mjs +174 -0
- package/src/swarm-cmd.mjs +424 -0
- package/src/tool-profiles.mjs +9 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Detection — Repo type detection and swarm manifest generation.
|
|
3
|
+
*
|
|
4
|
+
* Detects the repo type from directory structure and package metadata,
|
|
5
|
+
* then generates a swarm-manifest.json with non-overlapping domain assignments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
|
|
11
|
+
// ── Repo type detection ───────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const REPO_TYPE_SIGNALS = {
|
|
14
|
+
desktop: {
|
|
15
|
+
files: ["src-tauri/tauri.conf.json", "src-tauri/Cargo.toml"],
|
|
16
|
+
deps: ["@tauri-apps/api", "electron", "electron-builder"],
|
|
17
|
+
dirs: ["src-tauri"],
|
|
18
|
+
},
|
|
19
|
+
web: {
|
|
20
|
+
files: ["next.config.js", "next.config.mjs", "nuxt.config.ts", "vite.config.ts", "vite.config.js", "astro.config.mjs"],
|
|
21
|
+
deps: ["react-dom", "next", "nuxt", "vue", "@angular/core", "svelte", "astro"],
|
|
22
|
+
dirs: ["pages", "app", "views"],
|
|
23
|
+
},
|
|
24
|
+
mcp: {
|
|
25
|
+
files: [],
|
|
26
|
+
deps: ["@modelcontextprotocol/sdk"],
|
|
27
|
+
dirs: [],
|
|
28
|
+
},
|
|
29
|
+
cli: {
|
|
30
|
+
files: [],
|
|
31
|
+
deps: [],
|
|
32
|
+
dirs: [],
|
|
33
|
+
// CLI is the default fallback
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect the repo type from directory structure and package metadata.
|
|
39
|
+
* @param {string} cwd - Repository root directory
|
|
40
|
+
* @returns {"desktop"|"web"|"mcp"|"cli"|"monorepo"}
|
|
41
|
+
*/
|
|
42
|
+
export function detectRepoType(cwd) {
|
|
43
|
+
// Check for monorepo signals first
|
|
44
|
+
const hasWorkspaces = hasPackageWorkspaces(cwd);
|
|
45
|
+
const hasLerna = existsSync(join(cwd, "lerna.json"));
|
|
46
|
+
if (hasWorkspaces || hasLerna) return "monorepo";
|
|
47
|
+
|
|
48
|
+
const pkg = readPackageJson(cwd);
|
|
49
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
50
|
+
|
|
51
|
+
// Check each type in priority order
|
|
52
|
+
for (const type of ["desktop", "web", "mcp"]) {
|
|
53
|
+
const signals = REPO_TYPE_SIGNALS[type];
|
|
54
|
+
|
|
55
|
+
// Check files
|
|
56
|
+
for (const f of signals.files) {
|
|
57
|
+
if (existsSync(join(cwd, f))) return type;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check dependencies
|
|
61
|
+
for (const dep of signals.deps) {
|
|
62
|
+
if (allDeps[dep]) return type;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check directories
|
|
66
|
+
for (const dir of signals.dirs) {
|
|
67
|
+
if (existsSync(join(cwd, dir)) && statSync(join(cwd, dir)).isDirectory()) return type;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for Rust CLI (Cargo.toml without Tauri)
|
|
72
|
+
if (existsSync(join(cwd, "Cargo.toml")) && !existsSync(join(cwd, "src-tauri"))) return "cli";
|
|
73
|
+
|
|
74
|
+
// Check for Python CLI
|
|
75
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py"))) return "cli";
|
|
76
|
+
|
|
77
|
+
// Check for Go
|
|
78
|
+
if (existsSync(join(cwd, "go.mod"))) return "cli";
|
|
79
|
+
|
|
80
|
+
return "cli";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Domain assignment ─────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Default domain templates per repo type.
|
|
87
|
+
* Each domain defines glob patterns for file ownership.
|
|
88
|
+
*/
|
|
89
|
+
const DOMAIN_TEMPLATES = {
|
|
90
|
+
cli: [
|
|
91
|
+
{ id: "backend", role: "Swarm Backend Agent", patterns: ["src/**", "lib/**", "bin/**", "*.mjs", "*.js", "*.ts", "*.py", "*.rs", "*.go"] },
|
|
92
|
+
{ id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*"] },
|
|
93
|
+
{ id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*", "Makefile", "Dockerfile"] },
|
|
94
|
+
],
|
|
95
|
+
web: [
|
|
96
|
+
{ id: "backend", role: "Swarm Backend Agent", patterns: ["src/server/**", "src/api/**", "src/lib/**", "server/**", "api/**"] },
|
|
97
|
+
{ id: "frontend", role: "Swarm Frontend Agent", patterns: ["src/components/**", "src/pages/**", "src/app/**", "src/views/**", "src/styles/**", "public/**", "*.css", "*.html"] },
|
|
98
|
+
{ id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*", "e2e/**", "cypress/**"] },
|
|
99
|
+
{ id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*", "Makefile", "Dockerfile", "*.config.*"] },
|
|
100
|
+
],
|
|
101
|
+
desktop: [
|
|
102
|
+
{ id: "backend", role: "Swarm Backend Agent", patterns: ["src-tauri/**", "src/lib/**", "src/server/**"] },
|
|
103
|
+
{ id: "bridge", role: "Swarm Bridge Agent", patterns: ["src/bridge/**", "src/ipc/**", "src/commands/**"] },
|
|
104
|
+
{ id: "frontend", role: "Swarm Frontend Agent", patterns: ["src/components/**", "src/pages/**", "src/app/**", "src/views/**", "src/styles/**", "public/**"] },
|
|
105
|
+
{ id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*"] },
|
|
106
|
+
{ id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*", "Makefile", "Dockerfile"] },
|
|
107
|
+
],
|
|
108
|
+
mcp: [
|
|
109
|
+
{ id: "backend", role: "Swarm Backend Agent", patterns: ["src/**", "lib/**", "*.mjs", "*.js", "*.ts"] },
|
|
110
|
+
{ id: "tests", role: "Swarm Tests Agent", patterns: ["test/**", "tests/**", "spec/**", "__tests__/**", "*.test.*", "*.spec.*"] },
|
|
111
|
+
{ id: "infra", role: "Swarm Infra Agent", patterns: [".github/**", "*.md", "*.json", "*.yaml", "*.yml", "*.toml", ".eslintrc*", ".prettierrc*"] },
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate a swarm manifest for the given repo.
|
|
117
|
+
* @param {string} cwd - Repository root directory
|
|
118
|
+
* @param {object} [options]
|
|
119
|
+
* @param {string} [options.repoType] - Override auto-detected repo type
|
|
120
|
+
* @returns {object} Swarm manifest
|
|
121
|
+
*/
|
|
122
|
+
export function generateSwarmManifest(cwd, options = {}) {
|
|
123
|
+
const repoType = options.repoType || detectRepoType(cwd);
|
|
124
|
+
const templates = DOMAIN_TEMPLATES[repoType] || DOMAIN_TEMPLATES.cli;
|
|
125
|
+
|
|
126
|
+
// Build domains from templates
|
|
127
|
+
const domains = templates.map((tmpl, idx) => ({
|
|
128
|
+
id: tmpl.id,
|
|
129
|
+
role: tmpl.role,
|
|
130
|
+
patterns: tmpl.patterns,
|
|
131
|
+
agentSlot: idx,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// Read repo metadata
|
|
135
|
+
const pkg = readPackageJson(cwd);
|
|
136
|
+
const repoName = pkg.name || cwd.split(/[/\\]/).pop();
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
version: "1.0",
|
|
140
|
+
repo: repoName,
|
|
141
|
+
repoType,
|
|
142
|
+
domains,
|
|
143
|
+
stages: ["health-a", "health-b", "health-c", "feature", "treatment"],
|
|
144
|
+
exclusiveOwnership: {
|
|
145
|
+
mode: "strict",
|
|
146
|
+
maxAgentsPerWave: domains.length,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validate a swarm manifest for correctness.
|
|
153
|
+
* @param {object} manifest
|
|
154
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
155
|
+
*/
|
|
156
|
+
export function validateSwarmManifest(manifest) {
|
|
157
|
+
const issues = [];
|
|
158
|
+
|
|
159
|
+
if (!manifest) {
|
|
160
|
+
issues.push("Manifest is null or undefined");
|
|
161
|
+
return { valid: false, issues };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!manifest.version) issues.push("Missing version");
|
|
165
|
+
if (!manifest.repo) issues.push("Missing repo");
|
|
166
|
+
if (!Array.isArray(manifest.domains)) {
|
|
167
|
+
issues.push("Missing or invalid domains array");
|
|
168
|
+
return { valid: false, issues };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (manifest.domains.length === 0) {
|
|
172
|
+
issues.push("No domains defined — need at least 1");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (manifest.domains.length > 10) {
|
|
176
|
+
issues.push(`Too many domains (${manifest.domains.length}) — max 10`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for empty domains
|
|
180
|
+
for (const domain of manifest.domains) {
|
|
181
|
+
if (!domain.id) issues.push("Domain missing id");
|
|
182
|
+
if (!domain.role) issues.push(`Domain ${domain.id || "?"} missing role`);
|
|
183
|
+
if (!Array.isArray(domain.patterns) || domain.patterns.length === 0) {
|
|
184
|
+
issues.push(`Domain ${domain.id || "?"} has no patterns`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for duplicate domain IDs
|
|
189
|
+
const ids = manifest.domains.map(d => d.id);
|
|
190
|
+
const uniqueIds = new Set(ids);
|
|
191
|
+
if (uniqueIds.size !== ids.length) {
|
|
192
|
+
issues.push("Duplicate domain IDs found");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for pattern overlaps (simple heuristic — exact pattern match)
|
|
196
|
+
const allPatterns = [];
|
|
197
|
+
for (const domain of manifest.domains) {
|
|
198
|
+
for (const pattern of domain.patterns || []) {
|
|
199
|
+
const existing = allPatterns.find(p => p.pattern === pattern);
|
|
200
|
+
if (existing) {
|
|
201
|
+
issues.push(`Pattern "${pattern}" claimed by both ${existing.domain} and ${domain.id}`);
|
|
202
|
+
}
|
|
203
|
+
allPatterns.push({ pattern, domain: domain.id });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check stages
|
|
208
|
+
if (!Array.isArray(manifest.stages) || manifest.stages.length === 0) {
|
|
209
|
+
issues.push("Missing or empty stages array");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { valid: issues.length === 0, issues };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function readPackageJson(cwd) {
|
|
218
|
+
const pkgPath = join(cwd, "package.json");
|
|
219
|
+
if (!existsSync(pkgPath)) return {};
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
222
|
+
} catch {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function hasPackageWorkspaces(cwd) {
|
|
228
|
+
const pkg = readPackageJson(cwd);
|
|
229
|
+
return Array.isArray(pkg.workspaces) || (pkg.workspaces && pkg.workspaces.packages);
|
|
230
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Persistence Bridge — Optional connection to dogfood-labs.
|
|
3
|
+
*
|
|
4
|
+
* Converts swarm wave results into dogfood submission format and audit DB
|
|
5
|
+
* payloads. The core swarm mission works without this — it's activated by
|
|
6
|
+
* the --persist-evidence flag on `roleos swarm`.
|
|
7
|
+
*
|
|
8
|
+
* This mirrors the logic from dogfood-labs/tools/swarm/persist-results.js
|
|
9
|
+
* but produces the payloads without requiring dogfood-labs to be present.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Surface mapping ─────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map a domain ID to a product surface string for dogfood submissions.
|
|
16
|
+
* @param {string} domainId - e.g. "backend", "frontend", "tests"
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function surfaceFromDomain(domainId) {
|
|
20
|
+
const map = {
|
|
21
|
+
backend: "cli",
|
|
22
|
+
bridge: "cli",
|
|
23
|
+
tests: "cli",
|
|
24
|
+
infra: "cli",
|
|
25
|
+
frontend: "web",
|
|
26
|
+
};
|
|
27
|
+
return map[domainId] || "cli";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Verdict derivation ──────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Derive a verdict from findings.
|
|
34
|
+
* @param {{ severity: string, status?: string }[]} findings
|
|
35
|
+
* @returns {"pass"|"partial"|"fail"}
|
|
36
|
+
*/
|
|
37
|
+
export function deriveVerdict(findings) {
|
|
38
|
+
if (!findings || findings.length === 0) return "pass";
|
|
39
|
+
|
|
40
|
+
const open = findings.filter(f => f.status !== "fixed" && f.status !== "accepted_risk");
|
|
41
|
+
const hasCritical = open.some(f => f.severity === "critical");
|
|
42
|
+
const hasHigh = open.some(f => f.severity === "high");
|
|
43
|
+
|
|
44
|
+
if (hasCritical) return "fail";
|
|
45
|
+
if (hasHigh) return "partial";
|
|
46
|
+
return "pass";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Scenario results ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build per-domain scenario results from wave reports.
|
|
53
|
+
* @param {object[]} waveReports - Array of { domain, stage, findings, remediations }
|
|
54
|
+
* @returns {object[]}
|
|
55
|
+
*/
|
|
56
|
+
export function buildScenarioResults(waveReports) {
|
|
57
|
+
// Group by domain
|
|
58
|
+
const byDomain = {};
|
|
59
|
+
for (const wr of waveReports) {
|
|
60
|
+
if (!byDomain[wr.domain]) byDomain[wr.domain] = [];
|
|
61
|
+
byDomain[wr.domain].push(wr);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Object.entries(byDomain).map(([domain, reports]) => {
|
|
65
|
+
const allFindings = reports.flatMap(r => r.findings || []);
|
|
66
|
+
const allRemediations = reports.flatMap(r => r.remediations || []);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
scenario_id: `swarm-${domain}`,
|
|
70
|
+
product_surface: surfaceFromDomain(domain),
|
|
71
|
+
verdict: deriveVerdict(allFindings),
|
|
72
|
+
step_results: [
|
|
73
|
+
{ step: "audit", status: allFindings.length > 0 ? "pass" : "pass" },
|
|
74
|
+
{ step: "remediate", status: allRemediations.length > 0 ? "pass" : "skip" },
|
|
75
|
+
],
|
|
76
|
+
evidence: {
|
|
77
|
+
total_findings: allFindings.length,
|
|
78
|
+
open_findings: allFindings.filter(f => f.status !== "fixed").length,
|
|
79
|
+
fixed: allFindings.filter(f => f.status === "fixed").length,
|
|
80
|
+
severities: {
|
|
81
|
+
critical: allFindings.filter(f => f.severity === "critical").length,
|
|
82
|
+
high: allFindings.filter(f => f.severity === "high").length,
|
|
83
|
+
medium: allFindings.filter(f => f.severity === "medium").length,
|
|
84
|
+
low: allFindings.filter(f => f.severity === "low").length,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Overall verdict ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compute overall verdict from scenario results.
|
|
95
|
+
* @param {object[]} scenarioResults
|
|
96
|
+
* @returns {"pass"|"partial"|"fail"}
|
|
97
|
+
*/
|
|
98
|
+
export function computeOverallVerdict(scenarioResults) {
|
|
99
|
+
if (scenarioResults.some(s => s.verdict === "fail")) return "fail";
|
|
100
|
+
if (scenarioResults.some(s => s.verdict === "partial")) return "partial";
|
|
101
|
+
return "pass";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Dogfood submission payload ──────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a dogfood-labs-compatible submission payload.
|
|
108
|
+
* @param {object} manifest - Swarm manifest
|
|
109
|
+
* @param {object[]} waveReports - All wave reports from the run
|
|
110
|
+
* @param {object} meta - { commitSha, branch, startedAt, completedAt }
|
|
111
|
+
* @returns {object} Dogfood submission payload
|
|
112
|
+
*/
|
|
113
|
+
export function buildSubmission(manifest, waveReports, meta = {}) {
|
|
114
|
+
const scenarios = buildScenarioResults(waveReports);
|
|
115
|
+
const overall = computeOverallVerdict(scenarios);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
repo: manifest.repo || "unknown",
|
|
119
|
+
product_surface: surfaceFromDomain(manifest.domains?.[0]?.id || "backend"),
|
|
120
|
+
execution_mode: "automated",
|
|
121
|
+
overall_verdict: overall,
|
|
122
|
+
scenario_results: scenarios,
|
|
123
|
+
metadata: {
|
|
124
|
+
commit_sha: meta.commitSha || "unknown",
|
|
125
|
+
branch: meta.branch || "unknown",
|
|
126
|
+
started_at: meta.startedAt || new Date().toISOString(),
|
|
127
|
+
completed_at: meta.completedAt || new Date().toISOString(),
|
|
128
|
+
tool: "role-os-swarm",
|
|
129
|
+
tool_version: "1.0.0",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Audit DB payload ────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build an audit-DB-compatible payload from wave reports.
|
|
138
|
+
* @param {object} manifest - Swarm manifest
|
|
139
|
+
* @param {object[]} waveReports - All wave reports
|
|
140
|
+
* @param {object} meta - { commitSha, branch }
|
|
141
|
+
* @returns {object}
|
|
142
|
+
*/
|
|
143
|
+
export function buildAuditPayload(manifest, waveReports, meta = {}) {
|
|
144
|
+
const allFindings = waveReports.flatMap(r => r.findings || []);
|
|
145
|
+
const fixed = allFindings.filter(f => f.status === "fixed").length;
|
|
146
|
+
|
|
147
|
+
const severities = {
|
|
148
|
+
critical: allFindings.filter(f => f.severity === "critical").length,
|
|
149
|
+
high: allFindings.filter(f => f.severity === "high").length,
|
|
150
|
+
medium: allFindings.filter(f => f.severity === "medium").length,
|
|
151
|
+
low: allFindings.filter(f => f.severity === "low").length,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const scenarios = buildScenarioResults(waveReports);
|
|
155
|
+
const overall = computeOverallVerdict(scenarios);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
run: {
|
|
159
|
+
slug: `swarm-${manifest.repo || "unknown"}`,
|
|
160
|
+
commit_sha: meta.commitSha || "unknown",
|
|
161
|
+
scope_level: "full",
|
|
162
|
+
overall_status: fixed === allFindings.length ? "pass" : overall,
|
|
163
|
+
overall_posture: overall,
|
|
164
|
+
blocking_release: severities.critical > 0 || severities.high > 0,
|
|
165
|
+
},
|
|
166
|
+
findings: allFindings,
|
|
167
|
+
metrics: {
|
|
168
|
+
total_findings: allFindings.length,
|
|
169
|
+
...severities,
|
|
170
|
+
fixed,
|
|
171
|
+
pass_rate: allFindings.length > 0 ? Math.round((fixed / allFindings.length) * 100) : 100,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|