Skip to main content

libverify_core/
control.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::evidence::{EvidenceBundle, EvidenceGap, EvidenceState, RepositoryPosture};
6
7/// A string-based control identifier, enabling open extensibility.
8///
9/// Built-in controls use kebab-case IDs (e.g. "review-independence").
10/// Platform-specific verifiers can register controls with their own IDs
11/// (e.g. "jira-linkage", "bitbucket-pipeline-status").
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ControlId(String);
15
16impl ControlId {
17    pub fn new(id: impl Into<String>) -> Self {
18        Self(id.into())
19    }
20
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24}
25
26impl fmt::Display for ControlId {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.write_str(&self.0)
29    }
30}
31
32impl AsRef<str> for ControlId {
33    fn as_ref(&self) -> &str {
34        &self.0
35    }
36}
37
38impl std::borrow::Borrow<str> for ControlId {
39    fn borrow(&self) -> &str {
40        &self.0
41    }
42}
43
44impl From<&str> for ControlId {
45    fn from(s: &str) -> Self {
46        Self(s.to_string())
47    }
48}
49
50impl From<String> for ControlId {
51    fn from(s: String) -> Self {
52        Self(s)
53    }
54}
55
56// --- Built-in control IDs (constants for compile-time safety) ---
57
58pub mod builtin {
59    use super::ControlId;
60
61    // Source Track
62    pub const SOURCE_AUTHENTICITY: &str = "source-authenticity";
63    pub const REVIEW_INDEPENDENCE: &str = "review-independence";
64    pub const BRANCH_HISTORY_INTEGRITY: &str = "branch-history-integrity";
65    pub const BRANCH_PROTECTION_ENFORCEMENT: &str = "branch-protection-enforcement";
66    pub const TWO_PARTY_REVIEW: &str = "two-party-review";
67
68    // Build Track
69    pub const BUILD_PROVENANCE: &str = "build-provenance";
70    pub const REQUIRED_STATUS_CHECKS: &str = "required-status-checks";
71    pub const HOSTED_BUILD_PLATFORM: &str = "hosted-build-platform";
72    pub const PROVENANCE_AUTHENTICITY: &str = "provenance-authenticity";
73    pub const BUILD_ISOLATION: &str = "build-isolation";
74
75    // Dependencies Track
76    pub const DEPENDENCY_SIGNATURE: &str = "dependency-signature";
77    pub const DEPENDENCY_PROVENANCE_CHECK: &str = "dependency-provenance";
78    pub const DEPENDENCY_SIGNER_VERIFIED: &str = "dependency-signer-verified";
79    pub const DEPENDENCY_COMPLETENESS: &str = "dependency-completeness";
80
81    // Compliance (platform-neutral naming)
82    pub const CHANGE_REQUEST_SIZE: &str = "change-request-size";
83    pub const TEST_COVERAGE: &str = "test-coverage";
84    pub const SCOPED_CHANGE: &str = "scoped-change";
85    pub const ISSUE_LINKAGE: &str = "issue-linkage";
86    pub const STALE_REVIEW: &str = "stale-review";
87    pub const DESCRIPTION_QUALITY: &str = "description-quality";
88    pub const MERGE_COMMIT_POLICY: &str = "merge-commit-policy";
89    pub const CONVENTIONAL_TITLE: &str = "conventional-title";
90    pub const SECURITY_FILE_CHANGE: &str = "security-file-change";
91    pub const RELEASE_TRACEABILITY: &str = "release-traceability";
92
93    // ASPM / Repository Posture
94    pub const CODEOWNERS_COVERAGE: &str = "codeowners-coverage";
95    pub const SECRET_SCANNING: &str = "secret-scanning";
96    pub const VULNERABILITY_SCANNING: &str = "vulnerability-scanning";
97    pub const SECURITY_POLICY: &str = "security-policy";
98
99    // Enterprise Posture
100    pub const CODE_SCANNING_ALERTS_RESOLVED: &str = "code-scanning-alerts-resolved";
101    pub const RELEASE_ASSET_ATTESTATION: &str = "release-asset-attestation";
102    pub const PRIVILEGED_WORKFLOW_DETECTION: &str = "privileged-workflow-detection";
103    pub const SECURITY_TEST_IN_CI: &str = "security-test-in-ci";
104
105    // Supply Chain Transparency
106    pub const LICENSE_COMPLIANCE: &str = "license-compliance";
107    pub const SBOM_COMPLETENESS: &str = "sbom-completeness";
108
109    // Layer 2: Deterministic Gates
110    pub const HARNESS_GATE: &str = "harness-gate";
111    pub const COVERAGE_THRESHOLD: &str = "coverage-threshold";
112
113    // Container Image Attestation
114    pub const CONTAINER_SIGNATURE: &str = "container-signature";
115    pub const CONTAINER_PROVENANCE: &str = "container-provenance";
116
117    // Layer 3: Behavioral Diff
118    pub const BEHAVIORAL_REGRESSION: &str = "behavioral-regression";
119    pub const DEPLOYMENT_HEALTH: &str = "deployment-health";
120
121    // AI-ops (agent execution verification)
122    pub const AGENT_SPEC_CONFORMANCE: &str = "agent-spec-conformance";
123    pub const PRIVILEGED_OPERATION_AUDIT: &str = "privileged-operation-audit";
124    pub const MCP_SCOPE_CHECK: &str = "mcp-scope-check";
125    pub const NETWORK_EGRESS_AUDIT: &str = "network-egress-audit";
126
127    /// All 44 built-in control IDs.
128    pub const ALL: &[&str] = &[
129        SOURCE_AUTHENTICITY,
130        REVIEW_INDEPENDENCE,
131        BRANCH_HISTORY_INTEGRITY,
132        BRANCH_PROTECTION_ENFORCEMENT,
133        TWO_PARTY_REVIEW,
134        BUILD_PROVENANCE,
135        REQUIRED_STATUS_CHECKS,
136        HOSTED_BUILD_PLATFORM,
137        PROVENANCE_AUTHENTICITY,
138        BUILD_ISOLATION,
139        DEPENDENCY_SIGNATURE,
140        DEPENDENCY_PROVENANCE_CHECK,
141        DEPENDENCY_SIGNER_VERIFIED,
142        DEPENDENCY_COMPLETENESS,
143        CHANGE_REQUEST_SIZE,
144        TEST_COVERAGE,
145        SCOPED_CHANGE,
146        ISSUE_LINKAGE,
147        STALE_REVIEW,
148        DESCRIPTION_QUALITY,
149        MERGE_COMMIT_POLICY,
150        CONVENTIONAL_TITLE,
151        SECURITY_FILE_CHANGE,
152        RELEASE_TRACEABILITY,
153        CODEOWNERS_COVERAGE,
154        SECRET_SCANNING,
155        VULNERABILITY_SCANNING,
156        SECURITY_POLICY,
157        CODE_SCANNING_ALERTS_RESOLVED,
158        RELEASE_ASSET_ATTESTATION,
159        PRIVILEGED_WORKFLOW_DETECTION,
160        SECURITY_TEST_IN_CI,
161        LICENSE_COMPLIANCE,
162        SBOM_COMPLETENESS,
163        HARNESS_GATE,
164        COVERAGE_THRESHOLD,
165        CONTAINER_SIGNATURE,
166        CONTAINER_PROVENANCE,
167        BEHAVIORAL_REGRESSION,
168        DEPLOYMENT_HEALTH,
169        AGENT_SPEC_CONFORMANCE,
170        PRIVILEGED_OPERATION_AUDIT,
171        MCP_SCOPE_CHECK,
172        NETWORK_EGRESS_AUDIT,
173    ];
174
175    /// Returns a ControlId for a built-in constant.
176    pub fn id(s: &str) -> ControlId {
177        ControlId::new(s)
178    }
179}
180
181/// Outcome of evaluating a single control against evidence.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum ControlStatus {
185    Satisfied,
186    Violated,
187    Indeterminate,
188    NotApplicable,
189}
190
191impl ControlStatus {
192    pub fn as_str(&self) -> &'static str {
193        match self {
194            Self::Satisfied => "satisfied",
195            Self::Violated => "violated",
196            Self::Indeterminate => "indeterminate",
197            Self::NotApplicable => "not_applicable",
198        }
199    }
200}
201
202impl fmt::Display for ControlStatus {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        f.write_str(self.as_str())
205    }
206}
207
208/// Result of a single control evaluation.
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct ControlFinding {
211    pub control_id: ControlId,
212    pub status: ControlStatus,
213    pub rationale: String,
214    pub subjects: Vec<String>,
215    pub evidence_gaps: Vec<EvidenceGap>,
216}
217
218impl ControlFinding {
219    pub fn satisfied(
220        control_id: ControlId,
221        rationale: impl Into<String>,
222        subjects: Vec<String>,
223    ) -> Self {
224        Self {
225            control_id,
226            status: ControlStatus::Satisfied,
227            rationale: rationale.into(),
228            subjects,
229            evidence_gaps: Vec::new(),
230        }
231    }
232
233    pub fn violated(
234        control_id: ControlId,
235        rationale: impl Into<String>,
236        subjects: Vec<String>,
237    ) -> Self {
238        Self {
239            control_id,
240            status: ControlStatus::Violated,
241            rationale: rationale.into(),
242            subjects,
243            evidence_gaps: Vec::new(),
244        }
245    }
246
247    pub fn indeterminate(
248        control_id: ControlId,
249        rationale: impl Into<String>,
250        subjects: Vec<String>,
251        evidence_gaps: Vec<EvidenceGap>,
252    ) -> Self {
253        Self {
254            control_id,
255            status: ControlStatus::Indeterminate,
256            rationale: rationale.into(),
257            subjects,
258            evidence_gaps,
259        }
260    }
261
262    pub fn not_applicable(control_id: ControlId, rationale: impl Into<String>) -> Self {
263        Self {
264            control_id,
265            status: ControlStatus::NotApplicable,
266            rationale: rationale.into(),
267            subjects: Vec::new(),
268            evidence_gaps: Vec::new(),
269        }
270    }
271
272    /// Extracts `RepositoryPosture` from evidence, returning appropriate
273    /// `Indeterminate` or `NotApplicable` findings for non-complete states.
274    ///
275    /// Use in posture controls to eliminate repeated `match` boilerplate:
276    /// ```ignore
277    /// let posture = match ControlFinding::extract_posture(self.id(), evidence) {
278    ///     Ok(p) => p,
279    ///     Err(findings) => return findings,
280    /// };
281    /// ```
282    pub fn extract_posture(
283        id: ControlId,
284        evidence: &EvidenceBundle,
285    ) -> Result<&RepositoryPosture, Vec<ControlFinding>> {
286        match &evidence.repository_posture {
287            EvidenceState::Complete { value } | EvidenceState::Partial { value, .. } => Ok(value),
288            EvidenceState::Missing { gaps } => Err(vec![ControlFinding::indeterminate(
289                id,
290                "Repository posture evidence could not be collected",
291                vec![],
292                gaps.clone(),
293            )]),
294            EvidenceState::NotApplicable => Err(vec![ControlFinding::not_applicable(
295                id,
296                "Repository posture not applicable",
297            )]),
298        }
299    }
300}
301
302/// A verifiable SDLC control that produces findings from evidence.
303pub trait Control: Send + Sync {
304    /// Returns the unique identifier for this control.
305    fn id(&self) -> ControlId;
306
307    /// Human-readable description for SARIF rule output.
308    fn description(&self) -> &'static str {
309        "Custom control"
310    }
311
312    /// SOC2 Trust Services Criteria this control maps to (e.g., &["CC6.1", "CC8.1"]).
313    /// Returns empty slice for controls not mapped to SOC2.
314    fn tsc_criteria(&self) -> &'static [&'static str] {
315        builtin_tsc_mapping(self.id().as_str())
316    }
317
318    /// Actionable remediation hint shown when the control fails or needs review.
319    fn remediation_hint(&self) -> Option<&'static str> {
320        builtin_remediation_hint(self.id().as_str())
321    }
322
323    /// Evaluates the evidence bundle and returns one finding per subject.
324    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding>;
325}
326
327/// Returns an actionable remediation hint for a built-in control ID.
328pub fn builtin_remediation_hint(id: &str) -> Option<&'static str> {
329    match id {
330        builtin::SOURCE_AUTHENTICITY => Some("Sign commits: git config commit.gpgsign true"),
331        builtin::REVIEW_INDEPENDENCE => {
332            Some("Ensure PRs are reviewed by someone other than the author")
333        }
334        builtin::BRANCH_HISTORY_INTEGRITY => {
335            Some("Use linear history (rebase/squash, avoid merge commits)")
336        }
337        builtin::BRANCH_PROTECTION_ENFORCEMENT => {
338            Some("Enable branch protection rules at Settings > Branches")
339        }
340        builtin::TWO_PARTY_REVIEW => {
341            Some("Require at least 2 reviewers in branch protection rules")
342        }
343        builtin::REQUIRED_STATUS_CHECKS => {
344            Some("Add required status checks in branch protection rules")
345        }
346        builtin::BUILD_PROVENANCE => {
347            Some("Generate SLSA provenance with slsa-framework/slsa-github-generator")
348        }
349        builtin::HOSTED_BUILD_PLATFORM => Some("Use GitHub-hosted runners instead of self-hosted"),
350        builtin::PROVENANCE_AUTHENTICITY => {
351            Some("Verify build provenance signatures with cosign/slsa-verifier")
352        }
353        builtin::BUILD_ISOLATION => Some("Ensure builds run in ephemeral, isolated environments"),
354        builtin::DEPENDENCY_SIGNATURE => {
355            Some("Use signed dependencies; verify with cosign or sigstore")
356        }
357        builtin::DEPENDENCY_PROVENANCE_CHECK => {
358            Some("Ensure dependencies publish SLSA provenance attestations")
359        }
360        builtin::DEPENDENCY_SIGNER_VERIFIED => {
361            Some("Verify dependency signers against a trusted list")
362        }
363        builtin::DEPENDENCY_COMPLETENESS => {
364            Some("Ensure all transitive dependencies have provenance")
365        }
366        builtin::CHANGE_REQUEST_SIZE => Some(
367            "Keep PRs small and focused; split large changes. Monorepo cross-package PRs may false-positive here -- use --exclude change-request-size",
368        ),
369        builtin::TEST_COVERAGE => Some(
370            "Add or update tests for changed source files. Dependency-only PRs may false-positive here -- use --exclude test-coverage",
371        ),
372        builtin::SCOPED_CHANGE => Some(
373            "Limit PR to a single logical change; split unrelated changes. In monorepos, features spanning multiple packages are expected -- use --exclude scoped-change",
374        ),
375        builtin::ISSUE_LINKAGE => Some(
376            "Reference an issue in the PR body: Fixes #123 or Closes #456. Bot PRs (Dependabot/Renovate) don't link issues -- use --exclude issue-linkage",
377        ),
378        builtin::DESCRIPTION_QUALITY => {
379            Some("Add a meaningful PR description explaining the change")
380        }
381        builtin::MERGE_COMMIT_POLICY => {
382            Some("Use squash or rebase merge strategy instead of merge commits")
383        }
384        builtin::CONVENTIONAL_TITLE => Some(
385            "Use Conventional Commits format: type(scope): description. Bot PRs use their own title format -- use --exclude conventional-title",
386        ),
387        builtin::STALE_REVIEW => Some("Re-request review if changes were pushed after approval"),
388        builtin::SECURITY_FILE_CHANGE => {
389            Some("Security-sensitive file changes require additional review")
390        }
391        builtin::RELEASE_TRACEABILITY => Some("Link release to merged PRs and resolved issues"),
392        builtin::CODEOWNERS_COVERAGE => Some("Add a CODEOWNERS file to define code ownership"),
393        builtin::SECRET_SCANNING => {
394            Some("Enable secret scanning at Settings > Code security and analysis")
395        }
396        builtin::VULNERABILITY_SCANNING => {
397            Some("Enable Dependabot alerts at Settings > Code security and analysis")
398        }
399        builtin::SECURITY_POLICY => {
400            Some("Add a SECURITY.md file with vulnerability reporting instructions")
401        }
402        builtin::CODE_SCANNING_ALERTS_RESOLVED => {
403            Some("Resolve open code scanning alerts at Security > Code scanning alerts")
404        }
405        builtin::RELEASE_ASSET_ATTESTATION => {
406            Some("Attest release assets with gh attestation or sigstore/cosign")
407        }
408        builtin::PRIVILEGED_WORKFLOW_DETECTION => {
409            Some("Avoid pull_request_target with checkout of PR code in workflows")
410        }
411        builtin::SECURITY_TEST_IN_CI => {
412            Some("Add CodeQL or Semgrep to GitHub Actions: github/codeql-action/analyze")
413        }
414        builtin::LICENSE_COMPLIANCE => Some(
415            "Review copyleft dependencies (GPL, AGPL, SSPL) and replace with permissively-licensed alternatives or obtain legal approval",
416        ),
417        builtin::SBOM_COMPLETENESS => {
418            Some("Generate SBOM with syft, cyclonedx-cli, or cargo-sbom and attach to releases")
419        }
420        builtin::HARNESS_GATE => {
421            Some("Fix failing CI checks before merging. Run tests locally: cargo test / npm test")
422        }
423        builtin::COVERAGE_THRESHOLD => {
424            Some("Increase test coverage. Current coverage is below the minimum threshold")
425        }
426        builtin::CONTAINER_SIGNATURE => {
427            Some("Sign container images with cosign: cosign sign --yes ghcr.io/owner/repo:tag")
428        }
429        builtin::CONTAINER_PROVENANCE => Some(
430            "Generate SLSA provenance for container images using slsa-framework/slsa-github-generator or ko build --provenance",
431        ),
432        builtin::BEHAVIORAL_REGRESSION => Some(
433            "Investigate metric regressions post-deploy. Consider rolling back if latency increased >10% or error rate increased >5%",
434        ),
435        builtin::DEPLOYMENT_HEALTH => Some(
436            "Service health degraded post-deployment. Error rate exceeds 5% or availability below 99%. Consider immediate rollback",
437        ),
438        builtin::AGENT_SPEC_CONFORMANCE => Some(
439            "Define allowed_paths, forbidden_paths, and budget in agent spec to constrain agent scope",
440        ),
441        builtin::PRIVILEGED_OPERATION_AUDIT => Some(
442            "Review privileged git operations (force push, admin bypass, tag deletion) and restrict agent permissions",
443        ),
444        builtin::MCP_SCOPE_CHECK => Some(
445            "Restrict MCP tool access in agent spec. Add allowed_tools entries like 'mcp:github/*' or remove forbidden servers",
446        ),
447        builtin::NETWORK_EGRESS_AUDIT => Some(
448            "Review agent network access. Restrict outbound connections in agent spec or network policy",
449        ),
450        _ => None,
451    }
452}
453
454/// Returns SOC2 Trust Services Criteria for a built-in control ID.
455pub fn builtin_tsc_mapping(id: &str) -> &'static [&'static str] {
456    match id {
457        // CC6: Logical and Physical Access Controls
458        builtin::SOURCE_AUTHENTICITY => &["CC6.1"],
459        builtin::BRANCH_PROTECTION_ENFORCEMENT => &["CC6.1", "CC8.1"],
460        builtin::CODEOWNERS_COVERAGE => &["CC6.1"],
461        builtin::SECRET_SCANNING => &["CC6.1", "CC6.6"],
462        // CC7: System Operations
463        builtin::ISSUE_LINKAGE => &["CC7.2"],
464        builtin::STALE_REVIEW => &["CC7.2"],
465        builtin::SECURITY_FILE_CHANGE => &["CC7.2"],
466        builtin::RELEASE_TRACEABILITY => &["CC7.2"],
467        builtin::REQUIRED_STATUS_CHECKS => &["CC7.1"],
468        builtin::VULNERABILITY_SCANNING => &["CC7.1"],
469        builtin::SECURITY_POLICY => &["CC7.3", "CC7.4"],
470        // CC8: Change Management
471        builtin::REVIEW_INDEPENDENCE => &["CC8.1"],
472        builtin::TWO_PARTY_REVIEW => &["CC8.1"],
473        builtin::CHANGE_REQUEST_SIZE => &["CC8.1"],
474        builtin::TEST_COVERAGE => &["CC8.1"],
475        builtin::SCOPED_CHANGE => &["CC8.1"],
476        builtin::DESCRIPTION_QUALITY => &["CC8.1"],
477        builtin::MERGE_COMMIT_POLICY => &["CC8.1"],
478        builtin::CONVENTIONAL_TITLE => &["CC8.1"],
479        builtin::BRANCH_HISTORY_INTEGRITY => &["CC8.1"],
480        // PI: Processing Integrity
481        builtin::BUILD_PROVENANCE => &["PI1.4"],
482        builtin::HOSTED_BUILD_PLATFORM => &["PI1.4"],
483        builtin::PROVENANCE_AUTHENTICITY => &["PI1.4"],
484        builtin::BUILD_ISOLATION => &["PI1.4"],
485        // Dependencies (CC7.1 + PI)
486        builtin::DEPENDENCY_SIGNATURE => &["CC7.1", "PI1.4"],
487        builtin::DEPENDENCY_PROVENANCE_CHECK => &["CC7.1", "PI1.4"],
488        builtin::DEPENDENCY_SIGNER_VERIFIED => &["CC7.1", "PI1.4"],
489        builtin::DEPENDENCY_COMPLETENESS => &["CC7.1", "PI1.4"],
490        // Enterprise Posture
491        builtin::CODE_SCANNING_ALERTS_RESOLVED => &["CC7.1"],
492        builtin::RELEASE_ASSET_ATTESTATION => &["PI1.4"],
493        builtin::PRIVILEGED_WORKFLOW_DETECTION => &["CC6.1", "CC8.1"],
494        // Supply Chain Transparency
495        builtin::LICENSE_COMPLIANCE => &["CC7.1"],
496        builtin::SBOM_COMPLETENESS => &["CC7.1", "PI1.4"],
497        // Layer 2: Deterministic Gates
498        builtin::HARNESS_GATE => &["CC7.1", "CC8.1"],
499        builtin::COVERAGE_THRESHOLD => &["CC8.1"],
500        // Container Image Attestation
501        builtin::CONTAINER_SIGNATURE => &["PI1.4"],
502        builtin::CONTAINER_PROVENANCE => &["PI1.4"],
503        // Layer 3: Behavioral Diff
504        builtin::BEHAVIORAL_REGRESSION => &["CC7.1"],
505        builtin::DEPLOYMENT_HEALTH => &["CC7.1", "CC7.2"],
506        // AI-ops (agent execution verification)
507        builtin::AGENT_SPEC_CONFORMANCE => &["CC6.1", "CC8.1"],
508        builtin::PRIVILEGED_OPERATION_AUDIT => &["CC6.1", "CC7.2", "CC8.1"],
509        builtin::MCP_SCOPE_CHECK => &["CC6.1", "CC8.1"],
510        builtin::NETWORK_EGRESS_AUDIT => &["CC6.1", "CC6.6"],
511        _ => &[],
512    }
513}
514
515/// Runs every control against the evidence bundle and collects all findings.
516pub fn evaluate_all(
517    controls: &[Box<dyn Control>],
518    evidence: &EvidenceBundle,
519) -> Vec<ControlFinding> {
520    let mut findings = Vec::new();
521    for control in controls {
522        findings.extend(control.evaluate(evidence));
523    }
524    findings
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn control_id_display() {
533        let id = ControlId::new("review-independence");
534        assert_eq!(id.to_string(), "review-independence");
535        assert_eq!(id.as_str(), "review-independence");
536    }
537
538    #[test]
539    fn control_id_from_str() {
540        let id: ControlId = "source-authenticity".into();
541        assert_eq!(id.as_str(), "source-authenticity");
542    }
543
544    #[test]
545    fn all_builtins_have_remediation_hints() {
546        for id in builtin::ALL {
547            assert!(
548                builtin_remediation_hint(id).is_some(),
549                "missing remediation hint for built-in control: {id}"
550            );
551        }
552    }
553
554    #[test]
555    fn builtin_ids_are_unique() {
556        let mut seen = std::collections::HashSet::new();
557        for id in builtin::ALL {
558            assert!(seen.insert(id), "duplicate built-in ID: {id}");
559        }
560    }
561}