Export Flow

Reconciliation note (re-verified against src/ExportCenter, 2026-05-31). Export Center is a deterministic evidence-export engine built around tenant-scoped profiles that are run to produce signed, verifiable artifacts (canonical JSON, Trivy DB, mirror bundles, compliance dossiers) — not a Chromium-rendered “PDF/Excel report” service. The flow below is the profile → run → artifact model that the code defines (/v1/exports/*). Sections that describe behavior not present in the codebase are explicitly marked NOT IMPLEMENTED, ORPHANED, or Draft/roadmap.

Key implementation-status caveat: the HTTP API (profiles, runs, artifacts, verify, SSE, manifest) and the typed export surfaces (attestation, NIS2 SoA, risk bundle, audit/exception/lineage) are implemented, but the deployed Worker does not execute the generic profile→run phase pipeline — see the “Worker reality” note under Actors. Treat the worker-driven phases (§4) and the diagram’s worker column as the intended design, not live behaviour.

Overview

Export Center generates and delivers deterministic evidence exports: canonical JSON archives, Trivy-compatible vulnerability databases, full/delta mirror bundles, and compliance dossiers (CRA, NIS2, standards mappings). Exports are defined as reusable profiles and executed as runs; every run produces hashed artifacts plus a signed manifest that can be independently verified offline.

The unit of work is a profile (the what/how/where), and a run (a single deterministic execution of a profile). Runs are tenant-scoped, can be triggered ad-hoc, on a cron schedule, by an event, or via API, and converge to identical bundle bytes for identical inputs.

Audience: operators and auditors who configure export profiles and download evidence, and integrators building tools that consume or verify Export Center artifacts.

Business Value: Deterministic, signed, offline-verifiable exports give auditors and downstream tools reproducible evidence without depending on a live StellaOps instance.

Actors

ActorTypeRole
UserHumanCreates/activates profiles, starts runs, downloads artifacts
ConsoleSystemUI for profile and run management
Gateway / RouterServiceRoutes export requests (Stella Router microservice mesh)
ExportCenter (WebService)ServiceProfile/run lifecycle, artifact + manifest serving, verification, SSE
ExportCenter (Worker)ServiceBackground batch jobs (DevPortal offline bundle, risk bundle). See Worker reality note below — the generic profile→run phase pipeline is not driven by a deployed worker.
AdaptersLibraryTrivy DB / Java DB, Mirror delta, OCI distribution, compliance dossiers
EvidenceLockerServiceStores export manifest snapshots + distribution transcripts (optional)
Distribution targetsStorageFileSystem, OCI registry, Mirror, OfflineKit, S3/Azure/GCS (see note)

NOTE — RustFS, Scanner, Policy as direct flow actors. The original doc listed Scanner/Policy/RustFS as inline data sources queried during the flow. In code the run pipeline resolves a scope into items via the ExportScopeResolver/ExportPlanner (Core/Planner/*) and the configured adapters; there is no hard-coded GET /internal/scans/{id} / GET /internal/verdicts/{scan_id} orchestration in ExportCenter. RustFS is not referenced anywhere in src/ExportCenter. Artifact persistence is to the configured distribution targets / local artifact store (IExportArtifactStore), not a fixed RustFS layout.

NOT IMPLEMENTED — worker-driven run execution (the generic phase pipeline). The deployed StellaOps.ExportCenter.Worker host registers exactly two BackgroundServices (Worker/Program.cs): Worker (a one-shot DevPortal offline bundle job, DevPortalOfflineJob) and RiskBundleWorker (a one-shot risk bundle job). Each runs once if enabled, then idles on Task.Delay(Timeout.Infinite). There is no hosted service that polls the ExportRun table and drives a started run through ScopeResolution → … → Finalize. The ExportPlanner/ExportScopeResolver (Core/Planner/*) and the ExportPhaseKind model exist as building blocks but are not invoked by any deployed run-executor loop. Concretely: POST …/profiles/{id}/runs creates an ExportRun already in Running state (ExportApiEndpoints.StartRunFromProfile) but nothing in the deployed services transitions it to Completed, produces ExportArtifacts, signs, or distributes for the generic profile pipeline. The worker-column steps in the flow diagram below and the §4 phase list describe the intended engine, not behaviour wired into the shipped Worker. The artifact/manifest/verify/download read paths (§5–§7) and the typed export surfaces (attestation, NIS2 SoA, risk bundle, audit/exception/lineage — see “Other typed export surfaces”) are separately implemented and do produce output.

Prerequisites

Authorization (scopes)

Verified against StellaOpsScopes.cs and StellaOpsResourceServerPolicies.cs. Scope claim values are dot-form (no colon-form variant exists for Export Center):

Scope (claim value)Policy constantGrants
export.viewerExportViewerRead-only: list/get profiles, runs, artifacts; download; verify; manifest; attestation status; SSE streams
export.operatorExportOperatorCreate/update profiles; start runs; cancel runs
export.adminExportAdminArchive (soft-delete) profiles; retention/encryption/scheduling administration

The WebService applies a global fallback policy requiring authentication + export.viewer; write paths additionally require export.operator and archive requires export.admin. Health probes (/healthz, /readyz) are anonymous.

Supported Export Formats

Verified against ExportFormat (Core/Planner/ExportPlanModels.cs) and the profile catalogue (docs/modules/export-center/profiles.md).

Format enumDescription
JsonRawCanonical raw JSON / JSONL archives (advisories, VEX, SBOMs)
JsonPolicyJsonRaw plus policy snapshot + evaluated findings
NdjsonNewline-delimited JSON (streaming)
CsvCSV
MirrorFull mirror layout with indexes (full / delta variants)
TrivyDbTrivy vulnerability database (schema v2)
TrivyJavaDbTrivy Java DB supplement (Maven/Gradle/SBT)

Compression is independent (CompressionFormat: None, Gzip, Zstd, Brotli).

NOT IMPLEMENTED — PDF, Excel/.xlsx, SARIF, CycloneDX/SPDX as Export Center output formats. The original doc’s format table (PDF reports rendered via Chromium, Excel spreadsheets, SARIF, .cdx.json/.spdx.json direct outputs) does not correspond to any ExportFormat value or adapter in src/ExportCenter. Export Center emits evidence/data bundles, not human-readable rendered reports. SBOMs are carried as inputs/records (ExportInputKind.Sbom) inside JSON/mirror bundles, not as a standalone CycloneDX/SPDX export format selectable here. (SBOM document generation lives in the SBOM flow — see SBOM Generation Flow.)

Draft/roadmap — compliance dossier “formats”. CRA technical-file / conformity-dossier, NIS2 SoA / effectiveness reports, and standards mappings are real profile variants (see Adapters.Cra, Adapters.Nis2, Adapters.StandardsMapping and profiles.md), exposed via dedicated typed endpoints rather than a generic format selector.

Flow Diagram

The flow is profile-first: a profile is created and activated, then a run is started from that profile. The intended design is that the run is planned (scope resolution), executed by a worker (data fetch → transform → write → manifest → sign → distribute), and progress is streamed over SSE.

Read with the “Worker reality” note above. The ExportCenter Wkr column below (data-fetch → transform → write → manifest → sign → distribute) is the intended engine. In the shipped code the API creates the run in Running state, but no deployed background service executes these phases for the generic profile pipeline, so the run.completed/artifact steps are not driven end-to-end today. The diagram is retained as the target design.

┌──────────────────────────────────────────────────────────────────────────────────┐
│                              Export Flow (profile → run → artifact)                │
└──────────────────────────────────────────────────────────────────────────────────┘

┌────────┐  ┌─────────┐  ┌──────────────────┐  ┌───────────────────┐  ┌──────────────┐
│  User  │  │ Console │  │ ExportCenter API │  │ ExportCenter Wkr  │  │ Distribution │
└───┬────┘  └────┬────┘  └────────┬─────────┘  └─────────┬─────────┘  └──────┬───────┘
    │            │                │                      │                   │
    │ Create &   │ POST /v1/exports/profiles            │                   │
    │ activate   │ PUT  …/{id}  (status=Active)         │                   │
    │ profile    │───────────────>│                      │                   │
    │            │  201 / 200     │                      │                   │
    │<───────────│<───────────────│                      │                   │
    │            │                │                      │                   │
    │ Start run  │ POST /v1/exports/profiles/{id}/runs  │                   │
    │───────────>│───────────────>│                      │                   │
    │            │                │ (concurrency check;  │                   │
    │            │                │  Running or Queued)  │                   │
    │            │  202 Accepted  │  Location:           │                   │
    │            │  {runId,status}│  …/runs/{runId}      │                   │
    │<───────────│<───────────────│                      │                   │
    │            │                │                      │                   │
    │ Subscribe  │ GET …/runs/{runId}/events (SSE)      │                   │
    │───────────>│───────────────>│   plan: scope-resolve│                   │
    │            │  event:connected                      │                   │
    │            │  event:run.started                    │                   │
    │            │                │   data-fetch ────────>│                   │
    │            │  event:run.progress                   │ transform/redact  │
    │            │                │                      │ write output      │
    │            │                │                      │ generate manifest │
    │            │                │                      │ sign (optional)   │
    │            │                │                      │ distribute ──────>│
    │            │  event:run.completed                  │                   │
    │<───────────│<───────────────│                      │                   │
    │            │                │                      │                   │
    │ Download   │ GET …/runs/{runId}/artifacts/{aid}/download                │
    │───────────>│───────────────>│ (file stream)        │                   │
    │<───────────│<───────────────│                      │                   │
    │ Verify     │ POST …/runs/{runId}/verify           │                   │
    │───────────>│───────────────>│ (manifest+sig+hashes)│                   │
    │<───────────│<───────────────│                      │                   │

Step-by-Step

1. Create / activate an export profile

Profiles carry the scope, format, signing options, and (optional) cron schedule. A newly created profile starts in Draft; a run can only be started once it is moved to Active (via PUT).

POST /v1/exports/profiles HTTP/1.1
Authorization: Bearer {jwt}      # requires export.operator
Content-Type: application/json

{
  "name": "weekly-evidence-json",
  "description": "Canonical JSON evidence archive",
  "kind": "Scheduled",
  "scope": {
    "targetKinds": ["sbom", "vex", "advisory"],
    "namespaces": ["acme/payments"],
    "dateRange": { "from": "2026-05-01T00:00:00Z", "to": "2026-05-30T00:00:00Z", "field": "CreatedAt" },
    "maxItems": 5000
  },
  "format": {
    "format": "JsonPolicy",
    "compression": "Zstd",
    "sortKeys": true,
    "normalizeTimestamps": true
  },
  "signing": { "enabled": true, "algorithm": "ES256", "includeProvenance": true },
  "schedule": "0 8 * * MON"
}

Tenant identity: the WebService derives the tenant from the tenant_id (or tid) claim on the JWT, not from an X-Tenant-Id header. Requests without a resolvable tenant claim return 400. (The original doc’s X-Tenant-Id: acme-corp header is not how tenancy is resolved here.)

ExportProfileKind values: AdHoc, Scheduled, EventDriven, Continuous. ExportProfileStatus values: Draft, Active, Paused, Archived.

2. Start a run from the profile

POST /v1/exports/profiles/{profileId}/runs HTTP/1.1
Authorization: Bearer {jwt}      # requires export.operator
Content-Type: application/json

{
  "scopeOverride": null,
  "formatOverride": null,
  "correlationId": "trace-abc123",
  "dryRun": false
}

The API enforces per-tenant and per-profile concurrency limits (ExportConcurrencyOptions: default MaxConcurrentRunsPerTenant=4, MaxConcurrentRunsPerProfile=2, QueueExcessRuns=true, MaxQueueSizePerTenant=10). If limits are exceeded and queuing is disabled, the API returns 429 Too Many Requests.

On success it returns 202 Acceptedwith a Location header pointing at the run resource, and a body whose status is Running (or Queued if it had to be queued):

{
  "runId": "9f1c…",
  "profileId": "3a2b…",
  "tenantId": "11…",
  "status": "Running",
  "trigger": "Api",
  "correlationId": "trace-abc123",
  "initiatedBy": "user:alice",
  "progress": { "totalItems": 0, "processedItems": 0, "failedItems": 0, "totalSizeBytes": 0, "percentComplete": 0 },
  "createdAt": "2026-05-30T10:30:00Z",
  "startedAt": "2026-05-30T10:30:00Z",
  "expiresAt": "2026-06-06T10:30:00Z"
}

ExportRunStatus: Queued, Running, Completed, PartiallyCompleted, Failed, Cancelled. ExportRunTrigger: Manual, Scheduled, Event, Api. Runs default to a 7-day expiresAt (now.AddDays(7)). The API-created run is written directly in Running (or Queued) state with empty progress; the Trigger for runs started through POST …/runs is always Api.

Caveat (see “Worker reality”). The run is created Running, but for the generic profile pipeline no deployed worker subsequently advances it to Completed/Failed or produces artifacts. The status/progress fields and the §3–§4 planning model exist, but the end-to-end run engine is not wired in the shipped Worker host today.

3. Scope resolution & planning

The planner (Core/Planner/ExportPlanner.cs, ExportScopeResolver.cs) turns the profile’s ExportScope into a concrete set of ResolvedExportItems and an ExportPlan of ordered phases. ExportScope filters include: targetKinds, sourceRefs, tags, namespaces, dateRange, runIds, excludePatterns, maxItems, and deterministic sampling (None/Random/First/Last/Stratified/Systematic).

Each input is tracked as an ExportInput with ExportInputKind (Sbom, Vex, Attestation, ScanReport, PolicyResult, Evidence, RiskBundle, Advisory) and an ExportInputStatus (Pending/Processing/Processed/Failed/Skipped).

NOT IMPLEMENTED (as a fixed data-gather contract): the original “Data Gathering” table with GET /internal/scans/{id}, GET /internal/verdicts/{scan_id}, GET /internal/vex/applied/{scan_id}, GET /internal/sboms/{digest}. No such fixed internal-call sequence exists in src/ExportCenter; data is resolved via the scope resolver and adapters.

4. Execution phases

ExportPhaseKind (Core/Planner/ExportPlanModels.cs) defines the intended worker pipeline order. The ExportPlanner builds an ExportPlan of ExportPlanPhases in this order, but — per the “Worker reality” note above — no deployed background service currently executes the plan for the generic profile pipeline, so treat this list as the enumerated design, not a live runtime sequence:

1. ScopeResolution   – resolve scope and collect items
2. DataFetch         – fetch and transform source data
3. Transform         – normalization / redaction
4. WriteOutput       – write output files (JSON/Trivy/Mirror/…)
5. GenerateManifest  – compute checksums + manifest
6. Sign              – sign artifacts (when signing.enabled)
7. Distribute        – push to distribution targets
8. Verify            – verify distribution
9. ApplyRetention    – apply retention policy
10. Finalize         – cleanup / finalization

NOT IMPLEMENTED — Chromium PDF rendering. The original chromium.pdf({...}) snippet and “PDF generation uses Chromium for high-fidelity rendering” claim have no counterpart in src/ExportCenter. Removed.

Draft/roadmap — named report templates. compliance-executive, compliance-detailed, audit-evidence, developer-sarif, custom-{tenant} are not a configurable template registry in code. Profiles select a format and (for compliance) a typed adapter/variant; there is no free-form template name field beyond the profile configuration.

5. Artifacts & manifest

Each run produces one or more artifacts (ExportArtifact, Core/Persistence/IExportRunRepository.cs) with a Kind (free-form string), a content-addressed Checksum, a ChecksumAlgorithm string that defaults to "SHA-256" (it is a plain string field, not an enum), size, content type, and metadata. List/fetch/download:

GET  /v1/exports/runs/{runId}/artifacts                 # list
GET  /v1/exports/runs/{runId}/artifacts/{artifactId}    # metadata
GET  /v1/exports/runs/{runId}/artifacts/{artifactId}/download   # file stream

Download streams the file directly (FileStreamHttpResult) with the artifact’s content type and filename, and emits an audit event + download metric. The run manifest is available separately:

GET  /v1/exports/runs/{runId}/verify/manifest      # manifest content + digest

NOT IMPLEMENTED — fixed RustFS path layout & signed download URLs. The original blobs/exports/{tenant}/{yyyy}/{mm}/{exp-id}/… RustFS tree and the GET …/download → { "download_url": "https://storage…?sig=…" } signed-URL response are not what the code does. Download is a direct authenticated file stream from the artifact store; there is no signed-URL indirection and no hard-coded RustFS directory contract in src/ExportCenter.

6. Evidence locker integration (optional)

When configured, Export Center pushes a manifest snapshot to the Evidence Locker (IExportEvidenceLockerClient.PushSnapshotAsync) and later records a distribution transcript (UpdateDistributionTranscriptAsync). The locker returns a bundle id, a Merkle root hash, and an optional DSSE signature; Export Center can re-verify the root hash (VerifyRootHashAsync).

// shape returned by PushSnapshotAsync (ExportEvidenceSnapshotResult)
{
  "bundleId": "bnd-…",
  "rootHash": "sha256:…",
  "signature": { /* ExportDsseSignatureInfo: keyId, algorithm, signature, … */ }  // optional
}

The original “Evidence Sealing” JSON (bnd-789ghi, merkle_root, base64 signature, fixed contents array) was illustrative; the real contract is the IExportEvidenceLockerClient interface above. Detailed evidence packaging is covered in Evidence Bundle Export Flow.

7. Verification

Export Center can verify a completed run’s manifest, signatures, and content hashes — including optional Rekor transparency-log checks — without a live data source:

POST /v1/exports/runs/{runId}/verify HTTP/1.1
Authorization: Bearer {jwt}      # requires export.viewer
Content-Type: application/json

{
  "verifyHashes": true,
  "verifySignatures": true,
  "verifyManifest": true,
  "verifyEncryption": true,
  "checkRekor": false,
  "trustedKeys": []
}

Returns an ExportVerificationResponse with isValid, per-file hash results, manifest and signature results (algorithm, keyId, signer, optional Rekor index), errors, and warnings. A streaming variant is available at POST /v1/exports/runs/{runId}/verify/stream (SSE), and attestation status at GET /v1/exports/runs/{runId}/verify/attestation.

8. Distribution & delivery

Distribution is driven by ExportDistributionKind:

KindNotes
FileSystemLocal filesystem
MirrorMirror-server distribution (full/delta)
OfflineKitAir-gap offline-kit distribution
OciRegistryOCI artifact push (feature-flagged; disable for air-gap)
WebhookNotification (metadata only)
AmazonS3 / AzureBlob / GoogleCloudStorageObject-store targets

On-prem / air-gap posture. Per the module charter, OCI distribution must be disable-able for air-gap and tests must not reach the network. The cloud object-store kinds (AmazonS3, AzureBlob, GoogleCloudStorage) are opt-in distribution targets a tenant may configure; they are not defaults and Export Center stores only references/transcripts, never managing those services itself. Offline/file-system/mirror/OCI-to-a-local-registry are the first-class paths.

Draft/roadmap — email delivery & per-recipient delivery config. Delivery by email attachment/link is not implemented as an Export Center distribution kind (Webhook carries metadata only; there is no email kind). The original “Delivery” list (Download / Email / Webhook / S3) is partially aspirational — Download (direct stream) and Webhook exist; Email does not; S3 exists as a distribution target kind.

Export “Types” → profile kinds + input kinds

The original “Export Types” table (scan_report, sbom, vulnerability_report, policy_compliance, ai_code_guard_report, evidence_bundle, audit_log, …) does not map to a single type field on the request. In code:

ORPHANED — ai_code_guard_report export type. No such export type/profile exists in src/ExportCenter. Treat as NOT IMPLEMENTED.

Data Contracts

The original ExportRequest/ExportResponse TypeScript interfaces do not match the code. The actual contracts are (Api/ExportApiModels.cs):

Create profile (CreateExportProfileRequest)

interface CreateExportProfileRequest {
  name: string;                 // required, unique per tenant
  description?: string;
  kind?: "AdHoc" | "Scheduled" | "EventDriven" | "Continuous";   // default AdHoc
  scope?: ExportScope;
  format?: ExportFormatOptions;
  signing?: ExportSigningOptions;
  schedule?: string;            // cron
}

interface ExportFormatOptions {
  format?: "JsonRaw" | "JsonPolicy" | "Ndjson" | "Csv" | "Mirror" | "TrivyDb" | "TrivyJavaDb";
  compression?: "None" | "Gzip" | "Zstd" | "Brotli";
  includeMetadata?: boolean;    // default true
  prettyPrint?: boolean;
  redactFields?: string[];
  normalizeTimestamps?: boolean; // default true
  sortKeys?: boolean;            // default true
}

interface ExportSigningOptions {
  enabled?: boolean;
  algorithm?: string;           // default "ES256"
  keyId?: string;
  providerHint?: string;
  includeProvenance?: boolean;  // default true
}

Start run (StartExportRunRequest)

interface StartExportRunRequest {
  scopeOverride?: ExportScope;
  formatOverride?: ExportFormatOptions;
  correlationId?: string;
  dryRun?: boolean;
}

Run response (ExportRunResponse)

interface ExportRunResponse {
  runId: string;
  profileId: string;
  tenantId: string;
  status: "Queued" | "Running" | "Completed" | "PartiallyCompleted" | "Failed" | "Cancelled";
  trigger: "Manual" | "Scheduled" | "Event" | "Api";
  correlationId?: string;
  initiatedBy?: string;
  progress: { totalItems: number; processedItems: number; failedItems: number; totalSizeBytes: number; percentComplete: number };
  error?: { code: string; message: string; details?: Record<string,string> };
  createdAt: string;
  startedAt?: string;
  completedAt?: string;
  expiresAt?: string;
  artifacts?: Array<{ artifactId: string; name: string; kind: string; sizeBytes: number; contentType?: string; checksum?: string; downloadUrl?: string }>;
}

Endpoint reference (/v1/exports/*)

Verified against Api/ExportApiEndpoints.cs. The in-service route prefix is /v1/exports(a gateway may front it under /api). The legacy GET/POST/DELETE /exports endpoints are deprecated and return 410 Gone(Program.cs).

MethodPathScopePurpose
GET/v1/exports/profilesviewerList profiles (filter by status/kind/search; paginated)
GET/v1/exports/profiles/{profileId}viewerGet profile
POST/v1/exports/profilesoperatorCreate profile (201 Created)
PUT/v1/exports/profiles/{profileId}operatorUpdate profile
DELETE/v1/exports/profiles/{profileId}adminArchive (soft-delete) profile (204)
POST/v1/exports/profiles/{profileId}/runsoperatorStart run (202 Accepted; 429 if over limits)
GET/v1/exports/runsviewerList runs (filter by profile/status/trigger/date/correlation)
GET/v1/exports/runs/{runId}viewerGet run (+artifacts)
POST/v1/exports/runs/{runId}/canceloperatorCancel queued/running run
GET/v1/exports/runs/{runId}/artifactsviewerList artifacts
GET/v1/exports/runs/{runId}/artifacts/{artifactId}viewerArtifact metadata
GET/v1/exports/runs/{runId}/artifacts/{artifactId}/downloadviewerDownload (file stream)
GET/v1/exports/runs/{runId}/eventsviewerSSE run events
POST/v1/exports/runs/{runId}/verifyviewerVerify run
GET/v1/exports/runs/{runId}/verify/manifestviewerGet manifest
GET/v1/exports/runs/{runId}/verify/attestationviewerAttestation status
POST/v1/exports/runs/{runId}/verify/streamviewerStream verification (SSE)

Other typed export surfaces (same service, separate endpoint groups)

Registered in Program.cs alongside the core API; covered in depth by the module docs (docs/modules/export-center/*):

SSE run events

StreamRunEvents (GET /v1/exports/runs/{runId}/events) emits event: <type>\ndata: <json>\n\n. The ExportRunSseEventTypes constant class declares: run.started, run.progress, run.phase.started, run.phase.completed, run.artifact.created, run.completed, run.failed, run.cancelled.

Actually emitted by the handler (it polls the run row every 2s): the framing events connected and disconnected, plus run.progress (on ProcessedItems change) and a status-change event mapped to run.started (→Running), run.completed (→Completed/PartiallyCompleted), run.failed (→Failed), or run.cancelled (→Cancelled).

ORPHANED — run.phase.started, run.phase.completed, run.artifact.created. These three constants are declared but never emitted anywhere in src/ExportCenter (only defined in ExportApiModels.cs; not referenced by StreamRunEvents or any worker). They are unwired, awaiting the worker engine described in the “Worker reality” note. NOT IMPLEMENTED as emitted events today.

The original doc’s single WebSocket: export ready message is replaced by the typed SSE event stream above. Export Center uses Server-Sent Events, not WebSockets.

Scheduled Exports

Scheduling is a profile-level concern: a profile carries a single cron schedule string and a kind of Scheduled. Scheduled runs are triggered with ExportRunTrigger.Scheduled.

// the schedule lives on the profile, not in a separate "schedules:" config block
{
  "name": "weekly-evidence-json",
  "kind": "Scheduled",
  "schedule": "0 8 * * MON",
  "format": { "format": "JsonPolicy" },
  "scope": { "targetKinds": ["sbom", "vex", "advisory"] }
}

Draft/roadmap — declarative schedules:/delivery: YAML block. The original multi-schedule YAML with relative date_range (-7d/now), delivery.method: email, and templated subjects is not the implemented model. A profile has one cron string; relative date-range tokens and email delivery are not implemented (see formats/delivery notes above).

Error Handling

ConditionBehavior (verified)
Tenant claim missing400 BadRequest (tenant_not_found_in_claims)
Profile/run/artifact not found (or cross-tenant)404 NotFound
Start run on non-Active profile400 BadRequest (profile_not_active)
Duplicate profile name409 Conflict (profile_name_conflict)
Update an archived profile400 BadRequest (profile_archived)
Cancel run in non-cancellable state400 BadRequest (run_cancel_invalid_state)
Over concurrency / queue limits429 Too Many Requests (audited)
Run failureThe ExportRun model supports Failed/PartiallyCompleted with an ErrorJson (surfaced as error.code/message/details). For the generic profile pipeline these transitions are not driven today — no deployed worker advances or fails a started run (see the “Worker reality” note). The status enum + error contract are in place for when the engine is wired.

The original “Recovery” column (fall back to default template, simplify report and retry, retry with exponential backoff, queue-for-retry email) describes behavior not visible at the API layer and not implemented: there is no template registry, no PDF path, and (per the “Worker reality” note) no deployed run-executor that could perform worker-side retry/idempotency or retention pruning for the generic profile pipeline. Those remain roadmap.

Observability

Metrics

The meter is StellaOps.ExportCenter (ExportTelemetry.Meter, version 1.0.0); every instrument name is prefixed export_ (WebService/Telemetry/ExportTelemetry.cs). The four instruments the core profile/run/artifact/SSE API actually increments at runtime — with the exact tags the handlers pass — are:

Instrument nameC# fieldTypeTags emitted by the API
export_runs_totalExportRunsTotalCountertenant_id, profile_id
export_runs_in_progressExportRunsInProgressUpDownCountertenant_id
export_artifact_downloads_totalArtifactDownloadsTotalCountertenant_id, artifact_kind
export_sse_connections_totalSseConnectionsTotalCountertenant_id

ExportTelemetry also declares many more instruments for the typed surfaces and the (not-yet-wired) worker engine — e.g. counters export_runs_success_total, export_runs_failed_total, export_artifacts_total, export_bytes_total, export_concurrency_limit_exceeded_total, export_audit_events_total, the export_incidents_*/export_risk_bundle_jobs_*/export_timeline_events_* families, and histograms export_run_duration_seconds, export_plan_duration_seconds, export_bundle_size_bytes — see docs/modules/export-center/architecture.md (Observability) for the full list.

Corrected. The original metric names (export_requests_total, export_duration_seconds, export_size_bytes, export_failures_total with type/format/reason labels) are NOT the actual instrument names — treat them as NOT IMPLEMENTED. A prior reconciliation note also claimed “the canonical tag set is tenant/profile/adapter/result”; that is not what the code emits — the core API handlers tag with tenant_id / profile_id / artifact_kind (singular dimensions above). The adapter/result/phase dimensions appear in structured log fields, not as tags on these counters.

Trace / phase context

Phases map to ExportPhaseKind (see §4): ScopeResolution → DataFetch → Transform → WriteOutput → GenerateManifest → Sign → Distribute → Verify → ApplyRetention → Finalize.

Module references (source of truth)