sln build fix (again), tests fixes, audit work and doctors work

This commit is contained in:
master
2026-01-12 22:15:51 +02:00
parent 9873f80830
commit 9330c64349
812 changed files with 48051 additions and 3891 deletions

View File

@@ -60,7 +60,7 @@ Approval is recorded via Git forge review or a signed commit trailer
|-----------|------------|
| Technical deadlock | **Maintainer Summit** (recorded & published) |
| Security bug | Follow [Security Policy](SECURITY_POLICY.md) |
| Code of Conduct violation | See `CODE_OF_CONDUCT.md` escalation ladder |
| Code of Conduct violation | See `code-of-conduct/CODE_OF_CONDUCT.md` escalation ladder |
---

View File

@@ -1,88 +1,88 @@
# StellaOps CodeofConduct
*Contributor Covenant v2.1 + projectspecific escalation paths*
> We pledge to make participation in the StellaOps community a
> harassmentfree experience for everyone, regardless of age, body size,
> disability, ethnicity, sex characteristics, gender identity and expression,
> level of experience, education, socioeconomic status, nationality,
> personal appearance, race, religion, or sexual identity and orientation.
---
## 0·Our standard
This project adopts the
[**Contributor Covenant v2.1**](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
with the additions and clarifications listed below.
If anything here conflicts with the upstream covenant, *our additions win*.
---
## 1·Scope
| Applies to | Examples |
|------------|----------|
| **All official spaces** | Repos under `git.stella-ops.org/stella-ops.org/*`, Matrix rooms (`#stellaops:*`), issue trackers, pullrequest reviews, community calls, and any event officially sponsored by StellaOps |
| **Unofficial spaces that impact the project** | Public socialmedia posts that target or harass community members, coordinated harassment campaigns, doxxing, etc. |
---
## 2·Reporting a violation 
| Channel | When to use |
|---------|-------------|
# StellaOps CodeofConduct
*Contributor Covenant v2.1 + projectspecific escalation paths*
> We pledge to make participation in the StellaOps community a
> harassmentfree experience for everyone, regardless of age, body size,
> disability, ethnicity, sex characteristics, gender identity and expression,
> level of experience, education, socioeconomic status, nationality,
> personal appearance, race, religion, or sexual identity and orientation.
---
## 0·Our standard
This project adopts the
[**Contributor Covenant v2.1**](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
with the additions and clarifications listed below.
If anything here conflicts with the upstream covenant, *our additions win*.
---
## 1·Scope
| Applies to | Examples |
|------------|----------|
| **All official spaces** | Repos under `git.stella-ops.org/stella-ops.org/*`, Matrix rooms (`#stellaops:*`), issue trackers, pullrequest reviews, community calls, and any event officially sponsored by StellaOps |
| **Unofficial spaces that impact the project** | Public socialmedia posts that target or harass community members, coordinated harassment campaigns, doxxing, etc. |
---
## 2·Reporting a violation 
| Channel | When to use |
|---------|-------------|
| `conduct@stella-ops.org` (PGP key [`keys/#pgp`](https://stella-ops.org/keys/#pgp)) | **Primary, confidential** anything from microaggressions to serious harassment |
| Matrix `/msg @coc-bot:libera.chat` | Quick, inchat nudge for minor issues |
| Public issue with label `coc` | Transparency preferred and **you feel safe** doing so |
We aim to acknowledge **within 48hours** (business days, UTC).
---
## 3·Incident handlers 🛡
| Name | Role | Altcontact |
|------|------|-------------|
| Alice Doe (`@alice`) | Core Maintainer • Security WG | `+15550123` |
| Bob Ng (`@bob`) | UI Maintainer • Community lead | `+15550456` |
If **any** handler is the subject of a complaint, skip them and contact another
handler directly or email `conduct@stella-ops.org` only.
---
## 4·Enforcement ladder 
1. **Private coaches / mediation** first attempt to resolve misunderstandings.
2. **Warning** written, includes corrective actions & coolingoff period.
3. **Temporary exclusion** mute (chat), readonly (repo) for *N* days.
4. **Permanent ban** removal from all official spaces + revocation of roles.
All decisions are documented **privately** (for confidentiality) but a summary
is published quarterly in the “Community Health” report.
---
## 5·Appeals 🔄
A sanctioned individual may appeal **once** by emailing
`appeals@stella-ops.org` within **14days** of the decision.
Appeals are reviewed by **three maintainers not involved in the original case**
and resolved within 30days.
---
## 6·Noretaliation policy 🛑
Retaliation against reporters **will not be tolerated** and results in
immediate progression to **Step4** of the enforcement ladder.
---
## 7·Attribution & licence 📜
* Text adapted from ContributorCovenant v2.1
Copyright © 20142024 Contributor Covenant Contributors
Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
---
| Matrix `/msg @coc-bot:libera.chat` | Quick, inchat nudge for minor issues |
| Public issue with label `coc` | Transparency preferred and **you feel safe** doing so |
We aim to acknowledge **within 48hours** (business days, UTC).
---
## 3·Incident handlers 🛡
| Name | Role | Altcontact |
|------|------|-------------|
| Alice Doe (`@alice`) | Core Maintainer • Security WG | `+15550123` |
| Bob Ng (`@bob`) | UI Maintainer • Community lead | `+15550456` |
If **any** handler is the subject of a complaint, skip them and contact another
handler directly or email `conduct@stella-ops.org` only.
---
## 4·Enforcement ladder 
1. **Private coaches / mediation** first attempt to resolve misunderstandings.
2. **Warning** written, includes corrective actions & coolingoff period.
3. **Temporary exclusion** mute (chat), readonly (repo) for *N* days.
4. **Permanent ban** removal from all official spaces + revocation of roles.
All decisions are documented **privately** (for confidentiality) but a summary
is published quarterly in the “Community Health” report.
---
## 5·Appeals 🔄
A sanctioned individual may appeal **once** by emailing
`appeals@stella-ops.org` within **14days** of the decision.
Appeals are reviewed by **three maintainers not involved in the original case**
and resolved within 30days.
---
## 6·Noretaliation policy 🛑
Retaliation against reporters **will not be tolerated** and results in
immediate progression to **Step4** of the enforcement ladder.
---
## 7·Attribution & licence 📜
* Text adapted from ContributorCovenant v2.1
Copyright © 20142024 Contributor Covenant Contributors
Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
---

View File

@@ -0,0 +1,29 @@
# Testing Practices
## Scope
- Applies to all modules, shared libraries, and tooling in this repository.
- Covers quality, maintainability, security, reusability, and test readiness.
## Required test layers
- Unit tests for every library and service (happy paths, edge cases, determinism, serialization).
- Integration tests for cross-component flows (database, messaging, storage, and service contracts).
- End-to-end tests for user-visible workflows and release-critical flows.
- Performance tests for scanners, exporters, and release orchestration paths.
- Security tests for authn/authz, input validation, and dependency risk checks.
- Offline and airgap validation: all suites must run without network access.
## Cadence
- Per change: unit tests plus relevant integration tests and determinism checks.
- Nightly: full integration and end-to-end suites per module.
- Weekly: performance baselines and flakiness triage.
- Release gate: full test matrix, security verification, and reproducible build checks.
## Evidence and reporting
- Record results in sprint Execution Logs with date, scope, and outcomes.
- Track flaky tests and block releases until mitigations are documented.
- Store deterministic fixtures and hashes for any generated artifacts.
## Environment expectations
- Use UTC timestamps, fixed seeds, and CultureInfo.InvariantCulture where relevant.
- Avoid live network calls; rely on fixtures and local emulators only.
- Inject time and ID providers (TimeProvider, IGuidGenerator) for testability.

File diff suppressed because it is too large Load Diff

View File

@@ -1,326 +0,0 @@
# SPRINT INDEX: Release Orchestrator Implementation
> **Epic:** Stella Ops Suite - Release Control Plane
> **Batch:** 100
> **Status:** Planning
> **Created:** 10-Jan-2026
> **Source:** [Architecture Specification](../product/advisories/09-Jan-2026%20-%20Stella%20Ops%20Orchestrator%20Architecture.md)
---
## Overview
This sprint batch implements the **Release Orchestrator** - transforming Stella Ops from a vulnerability scanning platform into **Stella Ops Suite**, a unified release control plane for non-Kubernetes container environments.
### Business Value
- **Unified release governance:** Single pane of glass for release lifecycle
- **Audit-grade evidence:** Cryptographically signed proof of every decision
- **Security as a gate:** Reachability-aware scanning integrated into promotion flow
- **Plugin extensibility:** Support for any SCM, CI, registry, and vault
- **Non-K8s first:** Docker, Compose, ECS, Nomad deployment targets
### Key Principles
1. **Digest-first release identity** - Releases are immutable OCI digests, not tags
2. **Evidence for every decision** - Every promotion/deployment produces sealed evidence
3. **Pluggable everything, stable core** - Integrations are plugins; core is stable
4. **No feature gating** - All plans include all features
5. **Offline-first operation** - Core works in air-gapped environments
6. **Immutable generated artifacts** - Every deployment generates stored artifacts
---
## Implementation Phases
| Phase | Batch | Title | Description | Duration Est. |
|-------|-------|-------|-------------|---------------|
| 1 | 101 | Foundation | Database schema, plugin infrastructure | Foundation |
| 2 | 102 | Integration Hub | Connector runtime, built-in integrations | Foundation |
| 3 | 103 | Environment Manager | Environments, targets, agent registration | Core |
| 4 | 104 | Release Manager | Components, versions, release bundles | Core |
| 5 | 105 | Workflow Engine | DAG execution, step registry | Core |
| 6 | 106 | Promotion & Gates | Approvals, security gates, decisions | Core |
| 7 | 107 | Deployment Execution | Deploy orchestrator, artifact generation | Core |
| 8 | 108 | Agents | Docker, Compose, SSH, WinRM agents | Deployment |
| 9 | 109 | Evidence & Audit | Evidence packets, version stickers | Audit |
| 10 | 110 | Progressive Delivery | A/B releases, canary, traffic routing | Advanced |
| 11 | 111 | UI Implementation | Dashboard, workflow editor, screens | Frontend |
---
## Module Dependencies
```
┌──────────────┐
│ AUTHORITY │ (existing)
└──────┬───────┘
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ PLUGIN │ │ INTHUB │ │ ENVMGR │
│ (Batch 101) │ │ (Batch 102) │ │ (Batch 103) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└──────────┬───────┴──────────────────┘
┌───────────────┐
│ RELMAN │
│ (Batch 104) │
└───────┬───────┘
┌───────────────┐
│ WORKFL │
│ (Batch 105) │
└───────┬───────┘
┌──────────┴──────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ PROMOT │ │ DEPLOY │
│ (Batch 106) │ │ (Batch 107) │
└───────┬───────┘ └───────┬───────┘
│ │
│ ▼
│ ┌───────────────┐
│ │ AGENTS │
│ │ (Batch 108) │
│ └───────┬───────┘
│ │
└──────────┬──────────┘
┌───────────────┐
│ RELEVI │
│ (Batch 109) │
└───────┬───────┘
┌───────────────┐
│ PROGDL │
│ (Batch 110) │
└───────────────┘
```
---
## Sprint Structure
### Phase 1: Foundation (Batch 101)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 101_001 | Database Schema - Core Tables | DB | - |
| 101_002 | Plugin Registry | PLUGIN | 101_001 |
| 101_003 | Plugin Loader & Sandbox | PLUGIN | 101_002 |
| 101_004 | Plugin SDK | PLUGIN | 101_003 |
### Phase 2: Integration Hub (Batch 102)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 102_001 | Integration Manager | INTHUB | 101_002 |
| 102_002 | Connector Runtime | INTHUB | 102_001 |
| 102_003 | Built-in SCM Connectors | INTHUB | 102_002 |
| 102_004 | Built-in Registry Connectors | INTHUB | 102_002 |
| 102_005 | Built-in Vault Connector | INTHUB | 102_002 |
| 102_006 | Doctor Checks | INTHUB | 102_002 |
### Phase 3: Environment Manager (Batch 103)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 103_001 | Environment CRUD | ENVMGR | 101_001 |
| 103_002 | Target Registry | ENVMGR | 103_001 |
| 103_003 | Agent Manager - Core | ENVMGR | 103_002 |
| 103_004 | Inventory Sync | ENVMGR | 103_002, 103_003 |
### Phase 4: Release Manager (Batch 104)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 104_001 | Component Registry | RELMAN | 102_004 |
| 104_002 | Version Manager | RELMAN | 104_001 |
| 104_003 | Release Manager | RELMAN | 104_002 |
| 104_004 | Release Catalog | RELMAN | 104_003 |
### Phase 5: Workflow Engine (Batch 105)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 105_001 | Workflow Template Designer | WORKFL | 101_001 |
| 105_002 | Step Registry | WORKFL | 101_002 |
| 105_003 | Workflow Engine - DAG Executor | WORKFL | 105_001, 105_002 |
| 105_004 | Step Executor | WORKFL | 105_003 |
| 105_005 | Built-in Steps | WORKFL | 105_004 |
### Phase 6: Promotion & Gates (Batch 106)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 106_001 | Promotion Manager | PROMOT | 104_003, 103_001 |
| 106_002 | Approval Gateway | PROMOT | 106_001 |
| 106_003 | Gate Registry | PROMOT | 106_001 |
| 106_004 | Security Gate | PROMOT | 106_003 |
| 106_005 | Decision Engine | PROMOT | 106_002, 106_003 |
### Phase 7: Deployment Execution (Batch 107)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 107_001 | Deploy Orchestrator | DEPLOY | 105_003, 106_005 |
| 107_002 | Target Executor | DEPLOY | 107_001, 103_002 |
| 107_003 | Artifact Generator | DEPLOY | 107_001 |
| 107_004 | Rollback Manager | DEPLOY | 107_002 |
| 107_005 | Deployment Strategies | DEPLOY | 107_002 |
### Phase 8: Agents (Batch 108)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 108_001 | Agent Core Runtime | AGENTS | 103_003 |
| 108_002 | Agent - Docker | AGENTS | 108_001 |
| 108_003 | Agent - Compose | AGENTS | 108_002 |
| 108_004 | Agent - SSH | AGENTS | 108_001 |
| 108_005 | Agent - WinRM | AGENTS | 108_001 |
### Phase 9: Evidence & Audit (Batch 109)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 109_001 | Evidence Collector | RELEVI | 106_005, 107_001 |
| 109_002 | Evidence Signer | RELEVI | 109_001 |
| 109_003 | Version Sticker Writer | RELEVI | 107_002 |
| 109_004 | Audit Exporter | RELEVI | 109_002 |
### Phase 10: Progressive Delivery (Batch 110)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 110_001 | A/B Release Manager | PROGDL | 107_005 |
| 110_002 | Traffic Router Framework | PROGDL | 110_001 |
| 110_003 | Canary Controller | PROGDL | 110_002 |
| 110_004 | Router Plugin - Nginx | PROGDL | 110_002 |
### Phase 11: UI Implementation (Batch 111)
| Sprint ID | Title | Module | Dependencies |
|-----------|-------|--------|--------------|
| 111_001 | Dashboard - Overview | FE | 107_001 |
| 111_002 | Environment Management UI | FE | 103_001 |
| 111_003 | Release Management UI | FE | 104_003 |
| 111_004 | Workflow Editor | FE | 105_001 |
| 111_005 | Promotion & Approval UI | FE | 106_001 |
| 111_006 | Deployment Monitoring UI | FE | 107_001 |
| 111_007 | Evidence Viewer | FE | 109_002 |
---
## Documentation References
All architecture documentation is available in:
```
docs/modules/release-orchestrator/
├── README.md # Entry point
├── design/
│ ├── principles.md # Design principles
│ └── decisions.md # ADRs
├── modules/
│ ├── overview.md # Module landscape
│ ├── integration-hub.md # INTHUB spec
│ ├── environment-manager.md # ENVMGR spec
│ ├── release-manager.md # RELMAN spec
│ ├── workflow-engine.md # WORKFL spec
│ ├── promotion-manager.md # PROMOT spec
│ ├── deploy-orchestrator.md # DEPLOY spec
│ ├── agents.md # AGENTS spec
│ ├── progressive-delivery.md # PROGDL spec
│ ├── evidence.md # RELEVI spec
│ └── plugin-system.md # PLUGIN spec
├── data-model/
│ ├── schema.md # PostgreSQL schema
│ └── entities.md # Entity definitions
├── api/
│ └── overview.md # API design
├── workflow/
│ ├── templates.md # Template spec
│ ├── execution.md # Execution state machine
│ └── promotion.md # Promotion state machine
├── security/
│ ├── overview.md # Security architecture
│ ├── auth.md # AuthN/AuthZ
│ ├── agent-security.md # Agent security
│ └── threat-model.md # Threat model
├── deployment/
│ ├── overview.md # Deployment architecture
│ ├── strategies.md # Deployment strategies
│ └── artifacts.md # Artifact generation
├── integrations/
│ ├── overview.md # Integration types
│ ├── connectors.md # Connector interface
│ ├── webhooks.md # Webhook architecture
│ └── ci-cd.md # CI/CD patterns
├── operations/
│ ├── overview.md # Observability
│ └── metrics.md # Prometheus metrics
├── ui/
│ └── overview.md # UI specification
└── appendices/
├── glossary.md # Terms
├── errors.md # Error codes
└── evidence-schema.md # Evidence format
```
---
## Technology Stack
| Layer | Technology |
|-------|------------|
| Backend | .NET 10, C# preview |
| Database | PostgreSQL 16+ |
| Message Queue | RabbitMQ / Valkey |
| Frontend | Angular 17 |
| Agent Runtime | .NET AOT |
| Plugin Runtime | gRPC, container sandbox |
| Observability | OpenTelemetry, Prometheus |
---
## Risk Register
| Risk | Impact | Mitigation |
|------|--------|------------|
| Plugin security vulnerabilities | High | Sandbox isolation, capability restrictions |
| Agent compromise | High | mTLS, short-lived credentials, audit |
| Evidence tampering | High | Append-only DB, cryptographic signing |
| Registry unavailability | Medium | Connection pooling, caching, fallbacks |
| Complex workflow failures | Medium | Comprehensive testing, rollback support |
---
## Success Criteria
- [ ] Complete database schema for all 10 themes
- [ ] Plugin system supports connector, step, gate types
- [ ] At least 2 built-in connectors per integration type
- [ ] Environment → Release → Promotion → Deploy flow works E2E
- [ ] Evidence packet generated for every deployment
- [ ] Agent deploys to Docker and Compose targets
- [ ] UI shows pipeline overview, approval queues, deployment logs
- [ ] Performance: <500ms API P99, <5min deployment for 10 targets
---
## Execution Log
| Date | Entry |
|------|-------|
| 10-Jan-2026 | Sprint index created |
| | Architecture documentation complete |

View File

@@ -0,0 +1,258 @@
# SPRINT INDEX: Doctor Diagnostics System
> **Implementation ID:** 20260112
> **Batch ID:** 001
> **Phase:** Self-Service Diagnostics
> **Status:** TODO
> **Created:** 12-Jan-2026
---
## Overview
Implement a comprehensive **Doctor Diagnostics System** that enables self-service troubleshooting for Stella Ops deployments. This addresses the critical need for operators, DevOps engineers, and developers to diagnose, understand, and remediate issues without requiring deep platform knowledge or documentation familiarity.
### Problem Statement
Today's health check infrastructure is fragmented across 20+ services with inconsistent interfaces, no unified CLI entry point, and no actionable remediation guidance. Users cannot easily:
1. Diagnose what is working vs. what is failing
2. Understand why failures occur (evidence collection)
3. Fix issues without reading extensive documentation
4. Verify fixes with re-runnable checks
### Key Capabilities
1. **Unified Doctor Engine** - Plugin-based check execution with parallel processing
2. **48+ Diagnostic Checks** - Covering core, database, services, security, integrations, observability
3. **CLI Surface** - `stella doctor` command with rich filtering and output formats
4. **UI Surface** - Interactive doctor dashboard at `/ops/doctor`
5. **API Surface** - Programmatic access for CI/CD and monitoring integration
6. **Actionable Remediation** - Copy/paste fix commands with verification steps
### Architecture Decision
**Consolidate existing infrastructure, extend with plugin system:**
- Leverage existing `HealthCheckResult` from `StellaOps.Plugin.Abstractions`
- Extend existing `IDoctorCheck` from ReleaseOrchestrator IntegrationHub
- Leverage existing `IMigrationRunner` for database migration checks
- Reuse existing health endpoints for service graph checks
- Create new plugin discovery and execution framework
---
## Consolidation Strategy
### Phase 1: Foundation Consolidation
| Existing Component | Location | Action |
|-------------------|----------|--------|
| IDoctorCheck | IntegrationHub/Doctor | **Extend** - Add evidence and remediation |
| HealthCheckResult | Plugin.Abstractions | **Reuse** - Map to DoctorSeverity |
| DoctorReport | IntegrationHub/Doctor | **Extend** - Add remediation aggregation |
| IMigrationRunner | Infrastructure.Postgres | **Integrate** - Wrap in database plugin |
| CryptoProfileValidator | Cli/Services | **Migrate** - Move to core plugin |
| PlatformHealthService | Platform.Health | **Integrate** - Wire into service graph plugin |
### Phase 2: Plugin Implementation
| Plugin | Checks | Priority | Notes |
|--------|--------|----------|-------|
| Core | 9 | P0 | Config, runtime, disk, memory, time, crypto |
| Database | 8 | P0 | Connectivity, migrations, schema, pool |
| ServiceGraph | 6 | P1 | Gateway, routing, service health |
| Security | 9 | P1 | OIDC, LDAP, TLS, Vault |
| Integration.SCM | 8 | P2 | GitHub, GitLab connectivity/auth/permissions |
| Integration.Registry | 6 | P2 | Harbor, ECR connectivity/auth/pull |
| Observability | 4 | P3 | OTLP, logs, metrics |
| ReleaseOrchestrator | 4 | P3 | Environments, deployment targets |
### Phase 3: Surface Implementation
| Surface | Entry Point | Priority |
|---------|-------------|----------|
| CLI | `stella doctor` | P0 |
| API | `/api/v1/doctor/*` | P1 |
| UI | `/ops/doctor` | P2 |
---
## Sprint Structure
| Sprint | Module | Description | Status | Dependency |
|--------|--------|-------------|--------|------------|
| [001_001](SPRINT_20260112_001_001_DOCTOR_foundation.md) | LB | Doctor engine foundation and plugin framework | TODO | - |
| [001_002](SPRINT_20260112_001_002_DOCTOR_core_plugin.md) | LB | Core platform plugin (9 checks) | TODO | 001_001 |
| [001_003](SPRINT_20260112_001_003_DOCTOR_database_plugin.md) | LB | Database plugin (8 checks) | TODO | 001_001 |
| [001_004](SPRINT_20260112_001_004_DOCTOR_service_security_plugins.md) | LB | Service graph + security plugins (15 checks) | TODO | 001_001 |
| [001_005](SPRINT_20260112_001_005_DOCTOR_integration_plugins.md) | LB | SCM + registry plugins (14 checks) | TODO | 001_001 |
| [001_006](SPRINT_20260112_001_006_CLI_doctor_command.md) | CLI | `stella doctor` command implementation | TODO | 001_002 |
| [001_007](SPRINT_20260112_001_007_API_doctor_endpoints.md) | BE | Doctor API endpoints | TODO | 001_002 |
| [001_008](SPRINT_20260112_001_008_FE_doctor_dashboard.md) | FE | Angular doctor dashboard | TODO | 001_007 |
| [001_009](SPRINT_20260112_001_009_DOCTOR_self_service.md) | LB | Self-service features (export, scheduling) | TODO | 001_006 |
---
## Working Directory
```
src/
├── __Libraries/
│ └── StellaOps.Doctor/ # NEW - Core doctor engine
│ ├── Engine/
│ │ ├── DoctorEngine.cs
│ │ ├── CheckExecutor.cs
│ │ ├── CheckRegistry.cs
│ │ └── PluginLoader.cs
│ ├── Models/
│ │ ├── DoctorCheckResult.cs
│ │ ├── DoctorReport.cs
│ │ ├── Evidence.cs
│ │ ├── Remediation.cs
│ │ └── DoctorRunOptions.cs
│ ├── Plugins/
│ │ ├── IDoctorPlugin.cs
│ │ ├── IDoctorCheck.cs
│ │ ├── DoctorPluginContext.cs
│ │ └── DoctorCategory.cs
│ ├── Output/
│ │ ├── IReportFormatter.cs
│ │ ├── TextReportFormatter.cs
│ │ ├── JsonReportFormatter.cs
│ │ └── MarkdownReportFormatter.cs
│ └── DI/
│ └── DoctorServiceExtensions.cs
├── Doctor/ # NEW - Doctor module
│ └── __Plugins/
│ ├── StellaOps.Doctor.Plugin.Core/ # Core platform checks
│ ├── StellaOps.Doctor.Plugin.Database/ # Database checks
│ ├── StellaOps.Doctor.Plugin.ServiceGraph/ # Service health checks
│ ├── StellaOps.Doctor.Plugin.Security/ # Auth, TLS, secrets
│ ├── StellaOps.Doctor.Plugin.Scm/ # SCM integrations
│ ├── StellaOps.Doctor.Plugin.Registry/ # Registry integrations
│ └── StellaOps.Doctor.Plugin.Observability/ # Telemetry checks
│ └── StellaOps.Doctor.WebService/ # Doctor API host
│ └── __Tests/
│ └── StellaOps.Doctor.*.Tests/ # Test projects
├── Cli/
│ └── StellaOps.Cli/
│ └── Commands/
│ └── DoctorCommandGroup.cs # NEW
├── Web/
│ └── StellaOps.Web/
│ └── src/app/features/
│ └── doctor/ # NEW - Doctor UI
```
---
## Dependencies
| Dependency | Module | Status |
|------------|--------|--------|
| HealthCheckResult | Plugin.Abstractions | EXISTS |
| IDoctorCheck (existing) | IntegrationHub | EXISTS - Extend |
| IMigrationRunner | Infrastructure.Postgres | EXISTS |
| IIdentityProviderPlugin | Authority.Plugins | EXISTS |
| IIntegrationConnectorCapability | ReleaseOrchestrator.Plugin | EXISTS |
| PlatformHealthService | Platform.Health | EXISTS |
| CommandGroup pattern | Cli | EXISTS |
| Angular features pattern | Web | EXISTS |
---
## Check Catalog Summary
### Total: 48 Checks
| Category | Plugin | Check Count | Priority |
|----------|--------|-------------|----------|
| Core | stellaops.doctor.core | 9 | P0 |
| Database | stellaops.doctor.database | 8 | P0 |
| ServiceGraph | stellaops.doctor.servicegraph | 6 | P1 |
| Security | stellaops.doctor.security | 9 | P1 |
| Integration.SCM | stellaops.doctor.scm.* | 8 | P2 |
| Integration.Registry | stellaops.doctor.registry.* | 6 | P2 |
| Observability | stellaops.doctor.observability | 4 | P3 |
### Check ID Convention
```
check.{category}.{subcategory}.{specific}
```
Examples:
- `check.config.required`
- `check.database.migrations.pending`
- `check.services.gateway.routing`
- `check.integration.scm.github.auth`
---
## Success Criteria
- [ ] Doctor engine executes 48+ checks with parallel processing
- [ ] All checks produce evidence and remediation commands
- [ ] `stella doctor` CLI command with all filter options
- [ ] JSON/Markdown/Text output formats
- [ ] API endpoints for programmatic access
- [ ] UI dashboard with real-time updates
- [ ] Export capability for support tickets
- [ ] Unit test coverage >= 85%
- [ ] Integration tests for all plugins
- [ ] Documentation in `docs/doctor/`
---
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | All checks passed |
| 1 | One or more warnings |
| 2 | One or more failures |
| 3 | Doctor engine error |
| 4 | Invalid arguments |
| 5 | Timeout exceeded |
---
## Security Considerations
1. **Secret Redaction** - Connection strings, tokens, passwords never appear in output
2. **RBAC Scopes** - `doctor:run`, `doctor:run:full`, `doctor:export`, `admin:system`
3. **Audit Logging** - All doctor runs logged with user context
4. **Sensitive Checks** - Some checks require elevated permissions
---
## Decisions & Risks
| Decision/Risk | Status | Notes |
|---------------|--------|-------|
| Consolidate vs. replace existing health | DECIDED | Consolidate - reuse existing infrastructure |
| Plugin discovery: static vs dynamic | DECIDED | Static (DI registration) with optional dynamic loading |
| Check timeout handling | DECIDED | Per-check timeout with graceful cancellation |
| Remediation command safety | MITIGATED | Safety notes for destructive operations, backup recommendations |
| Multi-tenant check isolation | DEFERRED | Phase 2 - tenant-scoped checks |
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created from doctor-capabilities.md specification |
| 12-Jan-2026 | Consolidation strategy defined based on codebase analysis |
| | |
---
## Reference Documents
- **Specification:** `docs/doctor/doctor-capabilities.md`
- **Existing Doctor Service:** `src/ReleaseOrchestrator/__Libraries/.../IntegrationHub/Doctor/`
- **Health Abstractions:** `src/Plugin/StellaOps.Plugin.Abstractions/Health/`
- **Migration Framework:** `src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/`
- **Authority Plugins:** `src/Authority/StellaOps.Authority/StellaOps.Authority.Plugins.Abstractions/`

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,597 @@
# SPRINT: Doctor Core Plugin - Platform and Runtime Checks
> **Implementation ID:** 20260112
> **Sprint ID:** 001_002
> **Module:** LB (Library)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_001
---
## Overview
Implement the Core Platform plugin providing 9 fundamental diagnostic checks for configuration, runtime environment, and system resources.
---
## Working Directory
```
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Core/
```
---
## Check Catalog
| CheckId | Name | Severity | Tags | Description |
|---------|------|----------|------|-------------|
| `check.config.required` | Required Config | Fail | quick, config, startup | All required configuration values present |
| `check.config.syntax` | Config Syntax | Fail | quick, config | Configuration files have valid YAML/JSON |
| `check.config.deprecated` | Deprecated Config | Warn | config | No deprecated configuration keys in use |
| `check.runtime.dotnet` | .NET Runtime | Fail | quick, runtime | .NET version meets minimum requirements |
| `check.runtime.memory` | Memory | Warn | runtime, resources | Sufficient memory available |
| `check.runtime.disk.space` | Disk Space | Warn | runtime, resources | Sufficient disk space on required paths |
| `check.runtime.disk.permissions` | Disk Permissions | Fail | quick, runtime, security | Write permissions on required directories |
| `check.time.sync` | Time Sync | Warn | quick, runtime | System clock is synchronized (NTP) |
| `check.crypto.profiles` | Crypto Profiles | Fail | quick, security, crypto | Crypto profile valid, providers available |
---
## Deliverables
### Task 1: Plugin Structure
**Status:** TODO
```
StellaOps.Doctor.Plugin.Core/
├── CoreDoctorPlugin.cs
├── Checks/
│ ├── RequiredConfigCheck.cs
│ ├── ConfigSyntaxCheck.cs
│ ├── DeprecatedConfigCheck.cs
│ ├── DotNetRuntimeCheck.cs
│ ├── MemoryCheck.cs
│ ├── DiskSpaceCheck.cs
│ ├── DiskPermissionsCheck.cs
│ ├── TimeSyncCheck.cs
│ └── CryptoProfilesCheck.cs
├── Configuration/
│ ├── RequiredConfigKeys.cs
│ ├── DeprecatedConfigMapping.cs
│ └── ResourceThresholds.cs
└── StellaOps.Doctor.Plugin.Core.csproj
```
**CoreDoctorPlugin:**
```csharp
public sealed class CoreDoctorPlugin : IDoctorPlugin
{
public string PluginId => "stellaops.doctor.core";
public string DisplayName => "Core Platform";
public DoctorCategory Category => DoctorCategory.Core;
public Version Version => new(1, 0, 0);
public Version MinEngineVersion => new(1, 0, 0);
private readonly IReadOnlyList<IDoctorCheck> _checks;
public CoreDoctorPlugin()
{
_checks = new IDoctorCheck[]
{
new RequiredConfigCheck(),
new ConfigSyntaxCheck(),
new DeprecatedConfigCheck(),
new DotNetRuntimeCheck(),
new MemoryCheck(),
new DiskSpaceCheck(),
new DiskPermissionsCheck(),
new TimeSyncCheck(),
new CryptoProfilesCheck()
};
}
public bool IsAvailable(IServiceProvider services) => true;
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) => _checks;
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct)
=> Task.CompletedTask;
}
```
---
### Task 2: check.config.required
**Status:** TODO
Verify all required configuration values are present.
```csharp
public sealed class RequiredConfigCheck : IDoctorCheck
{
public string CheckId => "check.config.required";
public string Name => "Required Configuration";
public string Description => "Verify all required configuration values are present";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["quick", "config", "startup"];
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
private static readonly IReadOnlyList<RequiredConfigKey> RequiredKeys =
[
new("STELLAOPS_BACKEND_URL", "Backend API URL", "Environment or stellaops.yaml"),
new("ConnectionStrings:StellaOps", "Database connection", "Environment or stellaops.yaml"),
new("Authority:Issuer", "Authority issuer URL", "stellaops.yaml")
];
public bool CanRun(DoctorPluginContext context) => true;
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var missing = new List<RequiredConfigKey>();
var present = new List<string>();
foreach (var key in RequiredKeys)
{
var value = context.Configuration[key.Key];
if (string.IsNullOrEmpty(value))
missing.Add(key);
else
present.Add(key.Key);
}
if (missing.Count == 0)
{
return Task.FromResult(context.CreateResult(CheckId)
.Pass($"All {RequiredKeys.Count} required configuration values are present")
.WithEvidence(eb => eb
.Add("ConfiguredKeys", string.Join(", ", present))
.Add("TotalRequired", RequiredKeys.Count))
.Build());
}
return Task.FromResult(context.CreateResult(CheckId)
.Fail($"{missing.Count} required configuration value(s) missing")
.WithEvidence(eb =>
{
eb.Add("MissingKeys", string.Join(", ", missing.Select(k => k.Key)));
eb.Add("ConfiguredKeys", string.Join(", ", present));
foreach (var key in missing)
{
eb.Add($"Missing.{key.Key}", $"{key.Description} (source: {key.Source})");
}
})
.WithCauses(
"Environment variables not set",
"Configuration file not found or not loaded",
"Configuration section missing from stellaops.yaml")
.WithRemediation(rb => rb
.AddStep(1, "Check which configuration values are missing",
"stella config list --show-missing", CommandType.Shell)
.AddStep(2, "Set missing environment variables",
GenerateEnvExportCommands(missing), CommandType.Shell)
.AddStep(3, "Or update configuration file",
"# Edit: /etc/stellaops/stellaops.yaml", CommandType.FileEdit))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
private static string GenerateEnvExportCommands(List<RequiredConfigKey> missing)
{
var sb = new StringBuilder();
foreach (var key in missing)
{
sb.AppendLine($"export {key.Key}=\"{{VALUE}}\"");
}
return sb.ToString().TrimEnd();
}
}
internal sealed record RequiredConfigKey(string Key, string Description, string Source);
```
**Acceptance Criteria:**
- [ ] Checks all required keys
- [ ] Evidence includes missing and present keys
- [ ] Remediation generates export commands
---
### Task 3: check.config.syntax
**Status:** TODO
Verify configuration files have valid YAML/JSON syntax.
```csharp
public sealed class ConfigSyntaxCheck : IDoctorCheck
{
public string CheckId => "check.config.syntax";
public string Name => "Configuration Syntax";
public string Description => "Verify configuration files have valid YAML/JSON syntax";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["quick", "config"];
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(200);
private static readonly string[] ConfigPaths =
[
"/etc/stellaops/stellaops.yaml",
"/etc/stellaops/stellaops.json",
"stellaops.yaml",
"stellaops.json"
];
public bool CanRun(DoctorPluginContext context) => true;
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var errors = new List<ConfigSyntaxError>();
var validated = new List<string>();
foreach (var path in ConfigPaths)
{
if (!File.Exists(path)) continue;
try
{
var content = File.ReadAllText(path);
if (path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
{
ValidateYaml(content);
}
else if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
JsonDocument.Parse(content);
}
validated.Add(path);
}
catch (Exception ex)
{
errors.Add(new ConfigSyntaxError(path, ex.Message));
}
}
if (errors.Count == 0)
{
return Task.FromResult(context.CreateResult(CheckId)
.Pass($"All configuration files have valid syntax ({validated.Count} files)")
.WithEvidence(eb => eb.Add("ValidatedFiles", string.Join(", ", validated)))
.Build());
}
return Task.FromResult(context.CreateResult(CheckId)
.Fail($"{errors.Count} configuration file(s) have syntax errors")
.WithEvidence(eb =>
{
foreach (var error in errors)
{
eb.Add($"Error.{Path.GetFileName(error.Path)}", $"{error.Path}: {error.Message}");
}
})
.WithCauses(
"Invalid YAML indentation (tabs vs spaces)",
"JSON syntax error (missing comma, bracket)",
"File encoding issues (not UTF-8)")
.WithRemediation(rb => rb
.AddStep(1, "Validate YAML syntax", "yamllint /etc/stellaops/stellaops.yaml", CommandType.Shell)
.AddStep(2, "Check file encoding", "file /etc/stellaops/stellaops.yaml", CommandType.Shell)
.AddStep(3, "Fix common issues", "# Use spaces not tabs, check string quoting", CommandType.Manual))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
private static void ValidateYaml(string content)
{
var deserializer = new YamlDotNet.Serialization.Deserializer();
deserializer.Deserialize<object>(content);
}
}
internal sealed record ConfigSyntaxError(string Path, string Message);
```
---
### Task 4: check.runtime.dotnet
**Status:** TODO
Verify .NET runtime version meets minimum requirements.
```csharp
public sealed class DotNetRuntimeCheck : IDoctorCheck
{
public string CheckId => "check.runtime.dotnet";
public string Name => ".NET Runtime Version";
public string Description => "Verify .NET runtime version meets minimum requirements";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["quick", "runtime"];
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
private static readonly Version MinimumVersion = new(10, 0, 0);
public bool CanRun(DoctorPluginContext context) => true;
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var currentVersion = Environment.Version;
var runtimeInfo = RuntimeInformation.FrameworkDescription;
if (currentVersion >= MinimumVersion)
{
return Task.FromResult(context.CreateResult(CheckId)
.Pass($".NET {currentVersion} meets minimum requirement ({MinimumVersion})")
.WithEvidence(eb => eb
.Add("CurrentVersion", currentVersion.ToString())
.Add("MinimumVersion", MinimumVersion.ToString())
.Add("RuntimeDescription", runtimeInfo)
.Add("RuntimePath", RuntimeEnvironment.GetRuntimeDirectory()))
.Build());
}
return Task.FromResult(context.CreateResult(CheckId)
.Fail($".NET {currentVersion} is below minimum requirement ({MinimumVersion})")
.WithEvidence(eb => eb
.Add("CurrentVersion", currentVersion.ToString())
.Add("MinimumVersion", MinimumVersion.ToString())
.Add("RuntimeDescription", runtimeInfo))
.WithCauses(
"Outdated .NET runtime installed",
"Container image using old base",
"System package not updated")
.WithRemediation(rb => rb
.AddStep(1, "Check current .NET version", "dotnet --version", CommandType.Shell)
.AddStep(2, "Install required .NET version (Ubuntu/Debian)",
"wget https://dot.net/v1/dotnet-install.sh && chmod +x dotnet-install.sh && ./dotnet-install.sh --channel 10.0",
CommandType.Shell)
.AddStep(3, "Verify installation", "dotnet --list-runtimes", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
}
```
---
### Task 5: check.runtime.memory
**Status:** TODO
Check available memory.
```csharp
public sealed class MemoryCheck : IDoctorCheck
{
public string CheckId => "check.runtime.memory";
public string Name => "Available Memory";
public string Description => "Verify sufficient memory is available for operation";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["runtime", "resources"];
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(100);
private const long MinimumAvailableBytes = 1L * 1024 * 1024 * 1024; // 1 GB
private const long WarningAvailableBytes = 2L * 1024 * 1024 * 1024; // 2 GB
public bool CanRun(DoctorPluginContext context) => true;
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var gcInfo = GC.GetGCMemoryInfo();
var totalMemory = gcInfo.TotalAvailableMemoryBytes;
var availableMemory = totalMemory - GC.GetTotalMemory(forceFullCollection: false);
var evidence = context.CreateEvidence()
.Add("TotalMemory", FormatBytes(totalMemory))
.Add("AvailableMemory", FormatBytes(availableMemory))
.Add("GCHeapSize", FormatBytes(gcInfo.HeapSizeBytes))
.Add("GCFragmentation", $"{gcInfo.FragmentedBytes * 100.0 / gcInfo.HeapSizeBytes:F1}%")
.Build("Memory utilization metrics");
if (availableMemory < MinimumAvailableBytes)
{
return Task.FromResult(context.CreateResult(CheckId)
.Fail($"Critical: Only {FormatBytes(availableMemory)} available (minimum: {FormatBytes(MinimumAvailableBytes)})")
.WithEvidence(evidence)
.WithCauses(
"Memory leak in application",
"Insufficient container/VM memory allocation",
"Other processes consuming memory")
.WithRemediation(rb => rb
.AddStep(1, "Check current memory usage", "free -h", CommandType.Shell)
.AddStep(2, "Identify memory-heavy processes",
"ps aux --sort=-%mem | head -20", CommandType.Shell)
.AddStep(3, "Increase container memory limit (Docker)",
"docker update --memory 4g stellaops-gateway", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build());
}
if (availableMemory < WarningAvailableBytes)
{
return Task.FromResult(context.CreateResult(CheckId)
.Warn($"Low memory: {FormatBytes(availableMemory)} available (recommended: >{FormatBytes(WarningAvailableBytes)})")
.WithEvidence(evidence)
.WithCauses("High memory usage", "Growing heap size")
.WithRemediation(rb => rb
.AddStep(1, "Monitor memory usage", "watch -n 5 free -h", CommandType.Shell))
.Build());
}
return Task.FromResult(context.CreateResult(CheckId)
.Pass($"Memory OK: {FormatBytes(availableMemory)} available")
.WithEvidence(evidence)
.Build());
}
private static string FormatBytes(long bytes)
{
string[] suffixes = ["B", "KB", "MB", "GB", "TB"];
var i = 0;
var value = (double)bytes;
while (value >= 1024 && i < suffixes.Length - 1)
{
value /= 1024;
i++;
}
return $"{value:F1} {suffixes[i]}";
}
}
```
---
### Task 6: check.runtime.disk.space and check.runtime.disk.permissions
**Status:** TODO
Verify disk space and write permissions on required directories.
---
### Task 7: check.time.sync
**Status:** TODO
Verify system clock is synchronized.
```csharp
public sealed class TimeSyncCheck : IDoctorCheck
{
public string CheckId => "check.time.sync";
public string Name => "Time Synchronization";
public string Description => "Verify system clock is synchronized (NTP)";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["quick", "runtime"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
private const int MaxClockDriftSeconds = 5;
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
// Check against well-known NTP or HTTP time source
var systemTime = context.TimeProvider.GetUtcNow();
try
{
// Simple HTTP Date header check (fallback)
using var httpClient = context.Services.GetService<IHttpClientFactory>()
?.CreateClient("TimeCheck");
if (httpClient is null)
{
return context.CreateResult(CheckId)
.Skip("HTTP client not available for time check")
.Build();
}
var response = await httpClient.SendAsync(
new HttpRequestMessage(HttpMethod.Head, "https://www.google.com"), ct);
if (response.Headers.Date.HasValue)
{
var serverTime = response.Headers.Date.Value.UtcDateTime;
var drift = Math.Abs((systemTime.UtcDateTime - serverTime).TotalSeconds);
var evidence = context.CreateEvidence()
.Add("SystemTime", systemTime.ToString("O", CultureInfo.InvariantCulture))
.Add("ServerTime", serverTime.ToString("O", CultureInfo.InvariantCulture))
.Add("DriftSeconds", drift.ToString("F2", CultureInfo.InvariantCulture))
.Add("MaxAllowedDrift", MaxClockDriftSeconds.ToString(CultureInfo.InvariantCulture))
.Build("Time synchronization status");
if (drift > MaxClockDriftSeconds)
{
return context.CreateResult(CheckId)
.Warn($"Clock drift detected: {drift:F1}s (max allowed: {MaxClockDriftSeconds}s)")
.WithEvidence(evidence)
.WithCauses(
"NTP synchronization not enabled",
"NTP daemon not running",
"Network blocking NTP traffic")
.WithRemediation(rb => rb
.AddStep(1, "Check NTP status", "timedatectl status", CommandType.Shell)
.AddStep(2, "Enable NTP synchronization", "sudo timedatectl set-ntp true", CommandType.Shell)
.AddStep(3, "Force immediate sync", "sudo systemctl restart systemd-timesyncd", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
return context.CreateResult(CheckId)
.Pass($"Clock synchronized (drift: {drift:F2}s)")
.WithEvidence(evidence)
.Build();
}
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Warn($"Could not verify time sync: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
return context.CreateResult(CheckId)
.Skip("Could not determine time sync status")
.Build();
}
}
```
---
### Task 8: check.crypto.profiles
**Status:** TODO
Verify crypto profile is valid and providers are available.
**Migrate from:** `src/Cli/StellaOps.Cli/Services/CryptoProfileValidator.cs`
---
### Task 9: Test Suite
**Status:** TODO
```
src/Doctor/__Tests/StellaOps.Doctor.Plugin.Core.Tests/
├── CoreDoctorPluginTests.cs
├── Checks/
│ ├── RequiredConfigCheckTests.cs
│ ├── ConfigSyntaxCheckTests.cs
│ ├── DotNetRuntimeCheckTests.cs
│ ├── MemoryCheckTests.cs
│ ├── DiskSpaceCheckTests.cs
│ ├── DiskPermissionsCheckTests.cs
│ ├── TimeSyncCheckTests.cs
│ └── CryptoProfilesCheckTests.cs
└── Fixtures/
└── TestConfiguration.cs
```
---
## Acceptance Criteria (Sprint)
- [ ] All 9 checks implemented
- [ ] All checks produce evidence
- [ ] All checks produce remediation commands
- [ ] Plugin registered via DI
- [ ] Unit test coverage >= 85%
- [ ] No compiler warnings
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,509 @@
# SPRINT: Doctor Database Plugin - Connectivity and Migrations
> **Implementation ID:** 20260112
> **Sprint ID:** 001_003
> **Module:** LB (Library)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_001
---
## Overview
Implement the Database plugin providing 8 diagnostic checks for PostgreSQL connectivity, migration state, schema integrity, and connection pool health.
---
## Working Directory
```
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Database/
```
---
## Check Catalog
| CheckId | Name | Severity | Tags | Description |
|---------|------|----------|------|-------------|
| `check.database.connectivity` | DB Connectivity | Fail | quick, database | PostgreSQL connection successful |
| `check.database.version` | DB Version | Warn | database | PostgreSQL version meets requirements (>=16) |
| `check.database.migrations.pending` | Pending Migrations | Fail | database, migrations | No pending release migrations exist |
| `check.database.migrations.checksum` | Migration Checksums | Fail | database, migrations, security | Applied migration checksums match source |
| `check.database.migrations.lock` | Migration Locks | Warn | database, migrations | No stale migration locks exist |
| `check.database.schema.{schema}` | Schema Exists | Fail | database | Schema exists with expected tables |
| `check.database.connections.pool` | Connection Pool | Warn | database, performance | Connection pool healthy, not exhausted |
| `check.database.replication.lag` | Replication Lag | Warn | database | Replication lag within threshold |
---
## Deliverables
### Task 1: Plugin Structure
**Status:** TODO
```
StellaOps.Doctor.Plugin.Database/
├── DatabaseDoctorPlugin.cs
├── Checks/
│ ├── ConnectivityCheck.cs
│ ├── VersionCheck.cs
│ ├── PendingMigrationsCheck.cs
│ ├── MigrationChecksumCheck.cs
│ ├── MigrationLockCheck.cs
│ ├── SchemaExistsCheck.cs
│ ├── ConnectionPoolCheck.cs
│ └── ReplicationLagCheck.cs
├── Services/
│ ├── DatabaseHealthService.cs
│ └── MigrationStatusReader.cs
└── StellaOps.Doctor.Plugin.Database.csproj
```
**DatabaseDoctorPlugin:**
```csharp
public sealed class DatabaseDoctorPlugin : IDoctorPlugin
{
public string PluginId => "stellaops.doctor.database";
public string DisplayName => "Database";
public DoctorCategory Category => DoctorCategory.Database;
public Version Version => new(1, 0, 0);
public Version MinEngineVersion => new(1, 0, 0);
public bool IsAvailable(IServiceProvider services)
{
// Available if connection string is configured
var config = services.GetService<IConfiguration>();
return !string.IsNullOrEmpty(config?["ConnectionStrings:StellaOps"]);
}
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context)
{
var checks = new List<IDoctorCheck>
{
new ConnectivityCheck(),
new VersionCheck(),
new PendingMigrationsCheck(),
new MigrationChecksumCheck(),
new MigrationLockCheck(),
new ConnectionPoolCheck()
};
// Add schema checks for each configured module
var modules = GetConfiguredModules(context);
foreach (var module in modules)
{
checks.Add(new SchemaExistsCheck(module.SchemaName, module.ExpectedTables));
}
return checks;
}
public async Task InitializeAsync(DoctorPluginContext context, CancellationToken ct)
{
// Pre-warm connection pool
var factory = context.Services.GetService<NpgsqlDataSourceFactory>();
if (factory is not null)
{
await using var connection = await factory.OpenConnectionAsync(ct);
}
}
}
```
---
### Task 2: check.database.connectivity
**Status:** TODO
```csharp
public sealed class ConnectivityCheck : IDoctorCheck
{
public string CheckId => "check.database.connectivity";
public string Name => "Database Connectivity";
public string Description => "Verify PostgreSQL connection is successful";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["quick", "database"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var connectionString = context.Configuration["ConnectionStrings:StellaOps"];
if (string.IsNullOrEmpty(connectionString))
{
return context.CreateResult(CheckId)
.Fail("Database connection string not configured")
.WithEvidence(eb => eb.Add("ConfigKey", "ConnectionStrings:StellaOps"))
.WithCauses("Connection string not set in configuration")
.WithRemediation(rb => rb
.AddStep(1, "Set connection string environment variable",
"export STELLAOPS_POSTGRES_CONNECTION=\"Host=localhost;Database=stellaops;Username=stella_app;Password={PASSWORD}\"",
CommandType.Shell))
.Build();
}
var startTime = context.TimeProvider.GetUtcNow();
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
await using var connection = await dataSource.OpenConnectionAsync(ct);
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT version(), current_database(), current_user";
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
var version = reader.GetString(0);
var database = reader.GetString(1);
var user = reader.GetString(2);
var latency = context.TimeProvider.GetUtcNow() - startTime;
return context.CreateResult(CheckId)
.Pass($"PostgreSQL connection successful (latency: {latency.TotalMilliseconds:F0}ms)")
.WithEvidence(eb => eb
.AddConnectionString("Connection", connectionString)
.Add("ServerVersion", version)
.Add("Database", database)
.Add("User", user)
.Add("LatencyMs", latency.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture)))
.Build();
}
}
catch (NpgsqlException ex) when (ex.InnerException is SocketException)
{
return context.CreateResult(CheckId)
.Fail("Connection refused - PostgreSQL may not be running")
.WithEvidence(eb => eb
.AddConnectionString("Connection", connectionString)
.Add("Error", ex.Message))
.WithCauses(
"PostgreSQL service not running",
"Wrong hostname or port",
"Firewall blocking connection")
.WithRemediation(rb => rb
.AddStep(1, "Check PostgreSQL is running", "sudo systemctl status postgresql", CommandType.Shell)
.AddStep(2, "Check port binding", "sudo ss -tlnp | grep 5432", CommandType.Shell)
.AddStep(3, "Check firewall", "sudo ufw status | grep 5432", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (NpgsqlException ex) when (ex.SqlState == "28P01")
{
return context.CreateResult(CheckId)
.Fail("Authentication failed - check username and password")
.WithEvidence(eb => eb
.AddConnectionString("Connection", connectionString)
.Add("SqlState", ex.SqlState ?? "unknown")
.Add("Error", ex.Message))
.WithCauses(
"Wrong password",
"User does not exist",
"pg_hba.conf denying connection")
.WithRemediation(rb => rb
.AddStep(1, "Test connection manually",
"psql \"host=localhost dbname=stellaops user=stella_app\" -c \"SELECT 1\"",
CommandType.Shell)
.AddStep(2, "Check pg_hba.conf",
"sudo cat /etc/postgresql/16/main/pg_hba.conf | grep stellaops",
CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"Connection failed: {ex.Message}")
.WithEvidence(eb => eb
.AddConnectionString("Connection", connectionString)
.Add("Error", ex.Message)
.Add("ExceptionType", ex.GetType().Name))
.WithCauses("Unexpected connection error")
.Build();
}
return context.CreateResult(CheckId)
.Fail("Connection failed: no data returned")
.Build();
}
}
```
---
### Task 3: check.database.migrations.pending
**Status:** TODO
Integrate with existing `IMigrationRunner` from `StellaOps.Infrastructure.Postgres`.
```csharp
public sealed class PendingMigrationsCheck : IDoctorCheck
{
public string CheckId => "check.database.migrations.pending";
public string Name => "Pending Migrations";
public string Description => "Verify no pending release migrations exist";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["database", "migrations"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var migrationRunner = context.Services.GetService<IMigrationRunner>();
if (migrationRunner is null)
{
return context.CreateResult(CheckId)
.Skip("Migration runner not available")
.Build();
}
var allPending = new List<PendingMigrationInfo>();
// Check each module schema
var modules = new[] { "auth", "scanner", "orchestrator", "concelier", "policy" };
foreach (var module in modules)
{
var pending = await GetPendingMigrationsAsync(migrationRunner, module, ct);
allPending.AddRange(pending);
}
if (allPending.Count == 0)
{
return context.CreateResult(CheckId)
.Pass("No pending migrations")
.WithEvidence(eb => eb.Add("CheckedSchemas", string.Join(", ", modules)))
.Build();
}
var bySchema = allPending.GroupBy(p => p.Schema).ToList();
return context.CreateResult(CheckId)
.Fail($"{allPending.Count} pending migration(s) detected across {bySchema.Count} schema(s)")
.WithEvidence(eb =>
{
foreach (var group in bySchema)
{
eb.Add($"Schema.{group.Key}", string.Join(", ", group.Select(p => p.Name)));
}
eb.Add("TotalPending", allPending.Count);
})
.WithCauses(
"Release migrations not applied before deployment",
"Migration files added after last deployment",
"Schema out of sync with application version")
.WithRemediation(rb => rb
.WithSafetyNote("Always backup database before running migrations")
.RequiresBackup()
.AddStep(1, "Backup database first (RECOMMENDED)",
"pg_dump -h localhost -U stella_admin -d stellaops -F c -f stellaops_backup_$(date +%Y%m%d_%H%M%S).dump",
CommandType.Shell)
.AddStep(2, "Check migration status for all modules",
"stella system migrations-status",
CommandType.Shell)
.AddStep(3, "Apply pending release migrations",
"stella system migrations-run --category release",
CommandType.Shell)
.AddStep(4, "Verify all migrations applied",
"stella system migrations-status --verify",
CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
}
internal sealed record PendingMigrationInfo(string Schema, string Name, string Category);
```
---
### Task 4: check.database.migrations.checksum
**Status:** TODO
Verify applied migration checksums match source files.
---
### Task 5: check.database.migrations.lock
**Status:** TODO
Check for stale advisory locks.
```csharp
public sealed class MigrationLockCheck : IDoctorCheck
{
public string CheckId => "check.database.migrations.lock";
public string Name => "Migration Locks";
public string Description => "Verify no stale migration locks exist";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["database", "migrations"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var connectionString = context.Configuration["ConnectionStrings:StellaOps"];
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString!);
await using var connection = await dataSource.OpenConnectionAsync(ct);
await using var cmd = connection.CreateCommand();
// Check for advisory locks on migration lock keys
cmd.CommandText = @"
SELECT l.pid, l.granted, a.state, a.query,
NOW() - a.query_start AS duration
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.locktype = 'advisory'
AND l.objid IN (SELECT hashtext(schema_name || '_migrations')
FROM information_schema.schemata
WHERE schema_name LIKE 'stella%')";
var locks = new List<MigrationLock>();
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct))
{
locks.Add(new MigrationLock(
reader.GetInt32(0),
reader.GetBoolean(1),
reader.GetString(2),
reader.GetString(3),
reader.GetTimeSpan(4)));
}
if (locks.Count == 0)
{
return context.CreateResult(CheckId)
.Pass("No migration locks held")
.Build();
}
// Check if any locks are stale (held > 5 minutes with idle connection)
var staleLocks = locks.Where(l => l.Duration > TimeSpan.FromMinutes(5) && l.State == "idle").ToList();
if (staleLocks.Count > 0)
{
return context.CreateResult(CheckId)
.Warn($"{staleLocks.Count} stale migration lock(s) detected")
.WithEvidence(eb =>
{
foreach (var l in staleLocks)
{
eb.Add($"Lock.PID{l.Pid}", $"State: {l.State}, Duration: {l.Duration}");
}
})
.WithCauses(
"Migration process crashed while holding lock",
"Connection not properly closed after migration")
.WithRemediation(rb => rb
.AddStep(1, "Check for active locks",
"psql -d stellaops -c \"SELECT * FROM pg_locks WHERE locktype = 'advisory';\"",
CommandType.Shell)
.AddStep(2, "Identify lock holder process",
"psql -d stellaops -c \"SELECT pid, query, state FROM pg_stat_activity WHERE pid IN (SELECT pid FROM pg_locks WHERE locktype = 'advisory');\"",
CommandType.Shell)
.AddStep(3, "Clear stale lock (if process is dead)",
"# WARNING: Only if you are certain no migration is running\npsql -d stellaops -c \"SELECT pg_advisory_unlock_all();\"",
CommandType.SQL))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
return context.CreateResult(CheckId)
.Pass($"{locks.Count} active migration lock(s) - migrations in progress")
.WithEvidence(eb =>
{
foreach (var l in locks)
{
eb.Add($"Lock.PID{l.Pid}", $"State: {l.State}, Duration: {l.Duration}");
}
})
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"Could not check migration locks: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
}
}
internal sealed record MigrationLock(int Pid, bool Granted, string State, string Query, TimeSpan Duration);
```
---
### Task 6: check.database.connections.pool
**Status:** TODO
Check connection pool health.
---
### Task 7: check.database.schema.{schema}
**Status:** TODO
Dynamic check for each configured schema.
---
### Task 8: Test Suite
**Status:** TODO
```
src/Doctor/__Tests/StellaOps.Doctor.Plugin.Database.Tests/
├── DatabaseDoctorPluginTests.cs
├── Checks/
│ ├── ConnectivityCheckTests.cs
│ ├── PendingMigrationsCheckTests.cs
│ └── MigrationLockCheckTests.cs
└── Fixtures/
└── PostgresTestFixture.cs # Uses Testcontainers
```
---
## Dependencies
| Dependency | Package/Module | Status |
|------------|----------------|--------|
| Npgsql | Npgsql | EXISTS |
| IMigrationRunner | StellaOps.Infrastructure.Postgres | EXISTS |
| Testcontainers.PostgreSql | Testing | EXISTS |
---
## Acceptance Criteria (Sprint)
- [ ] All 8 checks implemented
- [ ] Integration with existing migration framework
- [ ] Connection string redaction in evidence
- [ ] Unit tests with Testcontainers
- [ ] Test coverage >= 85%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,661 @@
# SPRINT: Doctor Service Graph and Security Plugins
> **Implementation ID:** 20260112
> **Sprint ID:** 001_004
> **Module:** LB (Library)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_001
---
## Overview
Implement Service Graph and Security plugins providing 15 diagnostic checks for inter-service communication, authentication providers, TLS certificates, and secrets management.
---
## Working Directory
```
src/Doctor/__Plugins/
├── StellaOps.Doctor.Plugin.ServiceGraph/
└── StellaOps.Doctor.Plugin.Security/
```
---
## Check Catalog
### Service Graph Plugin (6 checks)
| CheckId | Name | Severity | Tags | Description |
|---------|------|----------|------|-------------|
| `check.services.gateway.running` | Gateway Running | Fail | quick, services | Gateway service running and accepting connections |
| `check.services.gateway.routing` | Gateway Routing | Fail | services, routing | Gateway can route to backend services |
| `check.services.{service}.health` | Service Health | Fail | services | Service health endpoint returns healthy |
| `check.services.{service}.connectivity` | Service Connectivity | Warn | services | Service reachable from gateway |
| `check.services.authority.connectivity` | Authority Connectivity | Fail | services, auth | Authority service reachable |
| `check.services.router.transport` | Router Transport | Warn | services | Router transport healthy |
### Security Plugin (9 checks)
| CheckId | Name | Severity | Tags | Description |
|---------|------|----------|------|-------------|
| `check.auth.oidc.discovery` | OIDC Discovery | Fail | auth, oidc | OIDC discovery endpoint accessible |
| `check.auth.oidc.jwks` | OIDC JWKS | Fail | auth, oidc | JWKS endpoint returns valid keys |
| `check.auth.ldap.bind` | LDAP Bind | Fail | auth, ldap | LDAP bind succeeds with service account |
| `check.auth.ldap.search` | LDAP Search | Warn | auth, ldap | LDAP search base accessible |
| `check.auth.ldap.groups` | LDAP Groups | Warn | auth, ldap | Group mapping functional |
| `check.tls.certificates.expiry` | TLS Expiry | Warn | security, tls | TLS certificates not expiring soon |
| `check.tls.certificates.chain` | TLS Chain | Fail | security, tls | TLS certificate chain valid |
| `check.secrets.vault.connectivity` | Vault Connectivity | Fail | security, vault | Vault server reachable |
| `check.secrets.vault.auth` | Vault Auth | Fail | security, vault | Vault authentication successful |
---
## Deliverables
### Task 1: Service Graph Plugin Structure
**Status:** TODO
```
StellaOps.Doctor.Plugin.ServiceGraph/
├── ServiceGraphDoctorPlugin.cs
├── Checks/
│ ├── GatewayRunningCheck.cs
│ ├── GatewayRoutingCheck.cs
│ ├── ServiceHealthCheck.cs
│ ├── ServiceConnectivityCheck.cs
│ ├── AuthorityConnectivityCheck.cs
│ └── RouterTransportCheck.cs
├── Services/
│ └── ServiceGraphHealthReader.cs
└── StellaOps.Doctor.Plugin.ServiceGraph.csproj
```
**ServiceGraphDoctorPlugin:**
```csharp
public sealed class ServiceGraphDoctorPlugin : IDoctorPlugin
{
public string PluginId => "stellaops.doctor.servicegraph";
public string DisplayName => "Service Graph";
public DoctorCategory Category => DoctorCategory.ServiceGraph;
public Version Version => new(1, 0, 0);
public Version MinEngineVersion => new(1, 0, 0);
private static readonly string[] CoreServices =
[
"gateway", "authority", "scanner", "orchestrator",
"concelier", "policy", "scheduler", "notifier"
];
public bool IsAvailable(IServiceProvider services) => true;
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context)
{
var checks = new List<IDoctorCheck>
{
new GatewayRunningCheck(),
new GatewayRoutingCheck(),
new AuthorityConnectivityCheck(),
new RouterTransportCheck()
};
// Add health checks for each configured service
foreach (var service in CoreServices)
{
checks.Add(new ServiceHealthCheck(service));
checks.Add(new ServiceConnectivityCheck(service));
}
return checks;
}
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct)
=> Task.CompletedTask;
}
```
---
### Task 2: check.services.gateway.running
**Status:** TODO
```csharp
public sealed class GatewayRunningCheck : IDoctorCheck
{
public string CheckId => "check.services.gateway.running";
public string Name => "Gateway Running";
public string Description => "Verify Gateway service is running and accepting connections";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["quick", "services"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var gatewayUrl = context.Configuration["Gateway:Url"] ?? "http://localhost:8080";
try
{
var httpClient = context.Services.GetRequiredService<IHttpClientFactory>()
.CreateClient("DoctorHealthCheck");
var response = await httpClient.GetAsync($"{gatewayUrl}/health/live", ct);
if (response.IsSuccessStatusCode)
{
return context.CreateResult(CheckId)
.Pass("Gateway is running and accepting connections")
.WithEvidence(eb => eb
.Add("GatewayUrl", gatewayUrl)
.Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture))
.Add("ResponseTime", response.Headers.Date?.ToString("O", CultureInfo.InvariantCulture) ?? "unknown"))
.Build();
}
return context.CreateResult(CheckId)
.Fail($"Gateway returned {(int)response.StatusCode} {response.ReasonPhrase}")
.WithEvidence(eb => eb
.Add("GatewayUrl", gatewayUrl)
.Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)))
.WithCauses(
"Gateway service unhealthy",
"Gateway dependencies failing")
.WithRemediation(rb => rb
.AddStep(1, "Check gateway logs", "sudo journalctl -u stellaops-gateway -n 100", CommandType.Shell)
.AddStep(2, "Restart gateway", "sudo systemctl restart stellaops-gateway", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (HttpRequestException ex)
{
return context.CreateResult(CheckId)
.Fail($"Cannot connect to Gateway: {ex.Message}")
.WithEvidence(eb => eb
.Add("GatewayUrl", gatewayUrl)
.Add("Error", ex.Message))
.WithCauses(
"Gateway service not running",
"Wrong gateway URL configured",
"Firewall blocking connection")
.WithRemediation(rb => rb
.AddStep(1, "Check service status", "sudo systemctl status stellaops-gateway", CommandType.Shell)
.AddStep(2, "Check port binding", "sudo ss -tlnp | grep 8080", CommandType.Shell)
.AddStep(3, "Start gateway", "sudo systemctl start stellaops-gateway", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
}
}
```
---
### Task 3: check.services.{service}.health
**Status:** TODO
Dynamic check for each service.
```csharp
public sealed class ServiceHealthCheck : IDoctorCheck
{
private readonly string _serviceName;
public ServiceHealthCheck(string serviceName)
{
_serviceName = serviceName;
}
public string CheckId => $"check.services.{_serviceName}.health";
public string Name => $"{Capitalize(_serviceName)} Health";
public string Description => $"Verify {_serviceName} service health endpoint returns healthy";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["services"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
public bool CanRun(DoctorPluginContext context)
{
// Skip if service is not configured
var serviceUrl = context.Configuration[$"Services:{Capitalize(_serviceName)}:Url"];
return !string.IsNullOrEmpty(serviceUrl);
}
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var serviceUrl = context.Configuration[$"Services:{Capitalize(_serviceName)}:Url"];
try
{
var httpClient = context.Services.GetRequiredService<IHttpClientFactory>()
.CreateClient("DoctorHealthCheck");
var startTime = context.TimeProvider.GetUtcNow();
var response = await httpClient.GetAsync($"{serviceUrl}/healthz", ct);
var latency = context.TimeProvider.GetUtcNow() - startTime;
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(ct);
return context.CreateResult(CheckId)
.Pass($"{Capitalize(_serviceName)} is healthy (latency: {latency.TotalMilliseconds:F0}ms)")
.WithEvidence(eb => eb
.Add("ServiceUrl", serviceUrl)
.Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture))
.Add("LatencyMs", latency.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture))
.Add("Response", content.Length > 500 ? content[..500] + "..." : content))
.Build();
}
return context.CreateResult(CheckId)
.Fail($"{Capitalize(_serviceName)} is unhealthy: {response.StatusCode}")
.WithEvidence(eb => eb
.Add("ServiceUrl", serviceUrl)
.Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)))
.WithCauses(
"Service dependencies failing",
"Database connection lost",
"Out of memory")
.WithRemediation(rb => rb
.AddStep(1, "Check service logs",
$"sudo journalctl -u stellaops-{_serviceName} -n 100", CommandType.Shell)
.AddStep(2, "Check detailed health",
$"curl -s {serviceUrl}/health/details | jq", CommandType.Shell)
.AddStep(3, "Restart service",
$"sudo systemctl restart stellaops-{_serviceName}", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"Cannot reach {_serviceName}: {ex.Message}")
.WithEvidence(eb => eb
.Add("ServiceUrl", serviceUrl)
.Add("Error", ex.Message))
.Build();
}
}
private static string Capitalize(string s) =>
string.IsNullOrEmpty(s) ? s : char.ToUpper(s[0], CultureInfo.InvariantCulture) + s[1..];
}
```
---
### Task 4: Security Plugin Structure
**Status:** TODO
```
StellaOps.Doctor.Plugin.Security/
├── SecurityDoctorPlugin.cs
├── Checks/
│ ├── OidcDiscoveryCheck.cs
│ ├── OidcJwksCheck.cs
│ ├── LdapBindCheck.cs
│ ├── LdapSearchCheck.cs
│ ├── LdapGroupsCheck.cs
│ ├── TlsExpiryCheck.cs
│ ├── TlsChainCheck.cs
│ ├── VaultConnectivityCheck.cs
│ └── VaultAuthCheck.cs
└── StellaOps.Doctor.Plugin.Security.csproj
```
---
### Task 5: check.auth.oidc.discovery
**Status:** TODO
```csharp
public sealed class OidcDiscoveryCheck : IDoctorCheck
{
public string CheckId => "check.auth.oidc.discovery";
public string Name => "OIDC Discovery";
public string Description => "Verify OIDC discovery endpoint is accessible";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["auth", "oidc"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
public bool CanRun(DoctorPluginContext context)
{
var issuer = context.Configuration["Authority:Issuer"];
return !string.IsNullOrEmpty(issuer);
}
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var issuer = context.Configuration["Authority:Issuer"]!;
var discoveryUrl = issuer.TrimEnd('/') + "/.well-known/openid-configuration";
try
{
var httpClient = context.Services.GetRequiredService<IHttpClientFactory>()
.CreateClient("DoctorHealthCheck");
var response = await httpClient.GetAsync(discoveryUrl, ct);
if (!response.IsSuccessStatusCode)
{
return context.CreateResult(CheckId)
.Fail($"OIDC discovery endpoint returned {response.StatusCode}")
.WithEvidence(eb => eb
.Add("DiscoveryUrl", discoveryUrl)
.Add("Issuer", issuer)
.Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)))
.WithCauses(
"Authority service not running",
"Wrong issuer URL configured",
"TLS certificate issue")
.WithRemediation(rb => rb
.AddStep(1, "Test discovery endpoint manually",
$"curl -v {discoveryUrl}", CommandType.Shell)
.AddStep(2, "Check Authority service",
"sudo systemctl status stellaops-authority", CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
var content = await response.Content.ReadAsStringAsync(ct);
var doc = JsonDocument.Parse(content);
// Validate required fields
var requiredFields = new[] { "issuer", "authorization_endpoint", "token_endpoint", "jwks_uri" };
var missingFields = requiredFields
.Where(f => !doc.RootElement.TryGetProperty(f, out _))
.ToList();
if (missingFields.Count > 0)
{
return context.CreateResult(CheckId)
.Warn($"OIDC discovery missing fields: {string.Join(", ", missingFields)}")
.WithEvidence(eb => eb
.Add("DiscoveryUrl", discoveryUrl)
.Add("MissingFields", string.Join(", ", missingFields)))
.Build();
}
return context.CreateResult(CheckId)
.Pass("OIDC discovery endpoint accessible and valid")
.WithEvidence(eb => eb
.Add("DiscoveryUrl", discoveryUrl)
.Add("Issuer", doc.RootElement.GetProperty("issuer").GetString()!)
.Add("JwksUri", doc.RootElement.GetProperty("jwks_uri").GetString()!))
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"Cannot reach OIDC discovery: {ex.Message}")
.WithEvidence(eb => eb
.Add("DiscoveryUrl", discoveryUrl)
.Add("Error", ex.Message))
.Build();
}
}
}
```
---
### Task 6: check.auth.ldap.bind
**Status:** TODO
Integrate with existing Authority LDAP plugin.
```csharp
public sealed class LdapBindCheck : IDoctorCheck
{
public string CheckId => "check.auth.ldap.bind";
public string Name => "LDAP Bind";
public string Description => "Verify LDAP bind succeeds with service account";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["auth", "ldap"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
public bool CanRun(DoctorPluginContext context)
{
var ldapHost = context.Configuration["Authority:Plugins:Ldap:Connection:Host"];
return !string.IsNullOrEmpty(ldapHost);
}
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var config = context.Configuration.GetSection("Authority:Plugins:Ldap");
var host = config["Connection:Host"]!;
var port = config.GetValue("Connection:Port", 636);
var bindDn = config["Connection:BindDn"]!;
var useTls = config.GetValue("Security:RequireTls", true);
try
{
// Use existing Authority LDAP plugin if available
var ldapPlugin = context.Services.GetService<IIdentityProviderPlugin>();
if (ldapPlugin is not null)
{
var healthResult = await ldapPlugin.CheckHealthAsync(ct);
if (healthResult.Status == AuthorityPluginHealthStatus.Healthy)
{
return context.CreateResult(CheckId)
.Pass("LDAP bind successful")
.WithEvidence(eb => eb
.Add("Host", host)
.Add("Port", port)
.Add("BindDn", bindDn)
.Add("UseTls", useTls))
.Build();
}
return context.CreateResult(CheckId)
.Fail($"LDAP bind failed: {healthResult.Message}")
.WithEvidence(eb => eb
.Add("Host", host)
.Add("Port", port)
.Add("BindDn", bindDn)
.Add("Error", healthResult.Message ?? "Unknown error"))
.WithCauses(
"Invalid bind credentials",
"LDAP server unreachable",
"TLS certificate issue",
"Firewall blocking LDAPS port")
.WithRemediation(rb => rb
.AddStep(1, "Test LDAP connection",
$"ldapsearch -H ldaps://{host}:{port} -D \"{bindDn}\" -W -b \"\" -s base",
CommandType.Shell)
.AddStep(2, "Check TLS certificate",
$"openssl s_client -connect {host}:{port} -showcerts",
CommandType.Shell)
.AddStep(3, "Verify credentials",
"# Check bind password in secrets store", CommandType.Manual))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
return context.CreateResult(CheckId)
.Skip("LDAP plugin not available")
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"LDAP check failed: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
}
}
```
---
### Task 7: check.tls.certificates.expiry
**Status:** TODO
Check TLS certificate expiration.
```csharp
public sealed class TlsExpiryCheck : IDoctorCheck
{
public string CheckId => "check.tls.certificates.expiry";
public string Name => "TLS Certificate Expiry";
public string Description => "Verify TLS certificates are not expiring soon";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["security", "tls"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
private const int WarningDays = 30;
private const int CriticalDays = 7;
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var certPaths = GetConfiguredCertPaths(context);
var now = context.TimeProvider.GetUtcNow();
var issues = new List<CertificateIssue>();
var healthy = new List<CertificateInfo>();
foreach (var path in certPaths)
{
if (!File.Exists(path)) continue;
try
{
var cert = X509Certificate2.CreateFromPemFile(path);
var daysRemaining = (cert.NotAfter - now.UtcDateTime).TotalDays;
var info = new CertificateInfo(
path,
cert.Subject,
cert.NotAfter,
(int)daysRemaining);
if (daysRemaining < CriticalDays)
{
issues.Add(new CertificateIssue(info, "critical"));
}
else if (daysRemaining < WarningDays)
{
issues.Add(new CertificateIssue(info, "warning"));
}
else
{
healthy.Add(info);
}
}
catch (Exception ex)
{
issues.Add(new CertificateIssue(
new CertificateInfo(path, "unknown", DateTime.MinValue, 0),
$"error: {ex.Message}"));
}
}
if (issues.Count == 0)
{
return context.CreateResult(CheckId)
.Pass($"All {healthy.Count} certificates valid (nearest expiry: {healthy.Min(c => c.DaysRemaining)} days)")
.WithEvidence(eb =>
{
foreach (var cert in healthy)
{
eb.Add($"Cert.{Path.GetFileName(cert.Path)}",
$"Expires: {cert.NotAfter:yyyy-MM-dd} ({cert.DaysRemaining} days)");
}
})
.Build();
}
var critical = issues.Where(i => i.Level == "critical").ToList();
var severity = critical.Count > 0 ? DoctorSeverity.Fail : DoctorSeverity.Warn;
return context.CreateResult(CheckId)
.WithSeverity(severity)
.WithDiagnosis($"{issues.Count} certificate(s) expiring soon or invalid")
.WithEvidence(eb =>
{
foreach (var issue in issues.OrderBy(i => i.Cert.DaysRemaining))
{
eb.Add($"Issue.{Path.GetFileName(issue.Cert.Path)}",
$"{issue.Level}: {issue.Cert.Subject}, expires {issue.Cert.NotAfter:yyyy-MM-dd} ({issue.Cert.DaysRemaining} days)");
}
})
.WithCauses(
"Certificate renewal not scheduled",
"ACME/Let's Encrypt automation not configured",
"Manual renewal overdue")
.WithRemediation(rb => rb
.AddStep(1, "Check certificate details",
$"openssl x509 -in {{CERT_PATH}} -noout -dates -subject",
CommandType.Shell)
.AddStep(2, "Renew certificate (certbot)",
"sudo certbot renew --cert-name stellaops.example.com",
CommandType.Shell)
.AddStep(3, "Restart services",
"sudo systemctl restart stellaops-gateway",
CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
private static IEnumerable<string> GetConfiguredCertPaths(DoctorPluginContext context)
{
// Common certificate locations
yield return "/etc/ssl/certs/stellaops.crt";
yield return "/etc/stellaops/tls/tls.crt";
// From configuration
var configPath = context.Configuration["Tls:CertificatePath"];
if (!string.IsNullOrEmpty(configPath))
yield return configPath;
}
}
internal sealed record CertificateInfo(string Path, string Subject, DateTime NotAfter, int DaysRemaining);
internal sealed record CertificateIssue(CertificateInfo Cert, string Level);
```
---
### Task 8: check.secrets.vault.connectivity
**Status:** TODO
Check Vault connectivity.
---
### Task 9: Test Suite
**Status:** TODO
---
## Acceptance Criteria (Sprint)
- [ ] Service Graph plugin with 6 checks
- [ ] Security plugin with 9 checks
- [ ] Integration with existing Authority plugins
- [ ] TLS certificate checking
- [ ] Test coverage >= 85%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,518 @@
# SPRINT: Doctor Integration Plugins - SCM and Registry
> **Implementation ID:** 20260112
> **Sprint ID:** 001_005
> **Module:** LB (Library)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_001
---
## Overview
Implement Integration plugins for SCM (GitHub, GitLab) and Container Registry (Harbor, ECR) providers. These plugins leverage the existing integration connector infrastructure from ReleaseOrchestrator.
---
## Working Directory
```
src/Doctor/__Plugins/
├── StellaOps.Doctor.Plugin.Scm/
└── StellaOps.Doctor.Plugin.Registry/
```
---
## Check Catalog
### SCM Plugin (8 checks)
| CheckId | Name | Severity | Tags | Description |
|---------|------|----------|------|-------------|
| `check.integration.scm.github.connectivity` | GitHub Connectivity | Fail | integration, scm | GitHub API reachable |
| `check.integration.scm.github.auth` | GitHub Auth | Fail | integration, scm | GitHub authentication valid |
| `check.integration.scm.github.permissions` | GitHub Permissions | Warn | integration, scm | Required permissions granted |
| `check.integration.scm.github.ratelimit` | GitHub Rate Limit | Warn | integration, scm | Rate limit not exhausted |
| `check.integration.scm.gitlab.connectivity` | GitLab Connectivity | Fail | integration, scm | GitLab API reachable |
| `check.integration.scm.gitlab.auth` | GitLab Auth | Fail | integration, scm | GitLab authentication valid |
| `check.integration.scm.gitlab.permissions` | GitLab Permissions | Warn | integration, scm | Required permissions granted |
| `check.integration.scm.gitlab.ratelimit` | GitLab Rate Limit | Warn | integration, scm | Rate limit not exhausted |
### Registry Plugin (6 checks)
| CheckId | Name | Severity | Tags | Description |
|---------|------|----------|------|-------------|
| `check.integration.registry.harbor.connectivity` | Harbor Connectivity | Fail | integration, registry | Harbor API reachable |
| `check.integration.registry.harbor.auth` | Harbor Auth | Fail | integration, registry | Harbor authentication valid |
| `check.integration.registry.harbor.pull` | Harbor Pull | Warn | integration, registry | Can pull from configured projects |
| `check.integration.registry.ecr.connectivity` | ECR Connectivity | Fail | integration, registry | ECR reachable |
| `check.integration.registry.ecr.auth` | ECR Auth | Fail | integration, registry | ECR authentication valid |
| `check.integration.registry.ecr.pull` | ECR Pull | Warn | integration, registry | Can pull from configured repos |
---
## Deliverables
### Task 1: Integration with Existing Infrastructure
**Status:** TODO
Leverage existing interfaces from ReleaseOrchestrator:
```csharp
// From src/ReleaseOrchestrator/__Libraries/.../IntegrationHub/
public interface IIntegrationConnectorCapability
{
Task<ConnectionTestResult> TestConnectionAsync(ConnectorContext context, CancellationToken ct);
Task<ConfigValidationResult> ValidateConfigAsync(JsonElement config, CancellationToken ct);
IReadOnlyList<string> GetSupportedOperations();
}
// Existing doctor checks from IntegrationHub
public interface IDoctorCheck // Existing
{
string Name { get; }
string Category { get; }
Task<CheckResult> ExecuteAsync(...);
}
```
**Strategy:** Create adapter plugins that wrap existing `IIntegrationConnectorCapability` implementations.
---
### Task 2: SCM Plugin Structure
**Status:** TODO
```
StellaOps.Doctor.Plugin.Scm/
├── ScmDoctorPlugin.cs
├── Checks/
│ ├── BaseScmCheck.cs
│ ├── ScmConnectivityCheck.cs
│ ├── ScmAuthCheck.cs
│ ├── ScmPermissionsCheck.cs
│ └── ScmRateLimitCheck.cs
├── Providers/
│ ├── GitHubCheckProvider.cs
│ └── GitLabCheckProvider.cs
└── StellaOps.Doctor.Plugin.Scm.csproj
```
**ScmDoctorPlugin:**
```csharp
public sealed class ScmDoctorPlugin : IDoctorPlugin
{
public string PluginId => "stellaops.doctor.scm";
public string DisplayName => "SCM Integrations";
public DoctorCategory Category => DoctorCategory.Integration;
public Version Version => new(1, 0, 0);
public Version MinEngineVersion => new(1, 0, 0);
public bool IsAvailable(IServiceProvider services)
{
// Available if any SCM integration is configured
var integrationManager = services.GetService<IIntegrationManager>();
return integrationManager is not null;
}
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context)
{
var checks = new List<IDoctorCheck>();
var integrationManager = context.Services.GetService<IIntegrationManager>();
if (integrationManager is null) return checks;
// Get all enabled SCM integrations
var scmIntegrations = integrationManager
.ListByTypeAsync(IntegrationType.Scm, CancellationToken.None)
.GetAwaiter().GetResult()
.Where(i => i.Enabled)
.ToList();
foreach (var integration in scmIntegrations)
{
var provider = integration.Provider.ToString().ToLowerInvariant();
checks.Add(new ScmConnectivityCheck(integration, provider));
checks.Add(new ScmAuthCheck(integration, provider));
checks.Add(new ScmPermissionsCheck(integration, provider));
checks.Add(new ScmRateLimitCheck(integration, provider));
}
return checks;
}
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct)
=> Task.CompletedTask;
}
```
---
### Task 3: check.integration.scm.github.connectivity
**Status:** TODO
```csharp
public sealed class ScmConnectivityCheck : IDoctorCheck
{
private readonly Integration _integration;
private readonly string _provider;
public ScmConnectivityCheck(Integration integration, string provider)
{
_integration = integration;
_provider = provider;
}
public string CheckId => $"check.integration.scm.{_provider}.connectivity";
public string Name => $"{Capitalize(_provider)} Connectivity";
public string Description => $"Verify {Capitalize(_provider)} API is reachable";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
public IReadOnlyList<string> Tags => ["integration", "scm"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var connectorFactory = context.Services.GetRequiredService<IConnectorFactory>();
var connector = await connectorFactory.CreateAsync(_integration, ct);
try
{
var testResult = await connector.TestConnectionAsync(
new ConnectorContext { TimeProvider = context.TimeProvider },
ct);
if (testResult.Success)
{
return context.CreateResult(CheckId)
.Pass($"{Capitalize(_provider)} API is reachable (latency: {testResult.LatencyMs}ms)")
.WithEvidence(eb => eb
.Add("Integration", _integration.Name)
.Add("Provider", _provider)
.Add("BaseUrl", _integration.Config.GetProperty("baseUrl").GetString() ?? "default")
.Add("LatencyMs", testResult.LatencyMs.ToString(CultureInfo.InvariantCulture)))
.Build();
}
return context.CreateResult(CheckId)
.Fail($"{Capitalize(_provider)} connection failed: {testResult.ErrorMessage}")
.WithEvidence(eb => eb
.Add("Integration", _integration.Name)
.Add("Provider", _provider)
.Add("Error", testResult.ErrorMessage ?? "Unknown error"))
.WithCauses(
$"{Capitalize(_provider)} API is down",
"Network connectivity issue",
"DNS resolution failure",
"Proxy configuration issue")
.WithRemediation(rb => rb
.AddStep(1, "Test API connectivity",
GetConnectivityCommand(_provider),
CommandType.Shell)
.AddStep(2, "Check DNS resolution",
$"nslookup {GetApiHost(_provider)}",
CommandType.Shell)
.AddStep(3, "Check firewall/proxy",
"curl -v --proxy $HTTP_PROXY " + GetApiHost(_provider),
CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"Connection test failed: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
}
private static string GetConnectivityCommand(string provider) => provider switch
{
"github" => "curl -s -o /dev/null -w '%{http_code}' https://api.github.com/zen",
"gitlab" => "curl -s -o /dev/null -w '%{http_code}' https://gitlab.com/api/v4/version",
_ => $"curl -s https://{provider}.com"
};
private static string GetApiHost(string provider) => provider switch
{
"github" => "api.github.com",
"gitlab" => "gitlab.com",
_ => $"{provider}.com"
};
private static string Capitalize(string s) =>
string.IsNullOrEmpty(s) ? s : char.ToUpper(s[0], CultureInfo.InvariantCulture) + s[1..];
}
```
---
### Task 4: check.integration.scm.github.ratelimit
**Status:** TODO
```csharp
public sealed class ScmRateLimitCheck : IDoctorCheck
{
private readonly Integration _integration;
private readonly string _provider;
public ScmRateLimitCheck(Integration integration, string provider)
{
_integration = integration;
_provider = provider;
}
public string CheckId => $"check.integration.scm.{_provider}.ratelimit";
public string Name => $"{Capitalize(_provider)} Rate Limit";
public string Description => $"Verify {Capitalize(_provider)} rate limit not exhausted";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["integration", "scm"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
private const int WarningThreshold = 100; // Warn when < 100 remaining
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var connectorFactory = context.Services.GetRequiredService<IConnectorFactory>();
var connector = await connectorFactory.CreateAsync(_integration, ct);
if (connector is not IRateLimitInfo rateLimitConnector)
{
return context.CreateResult(CheckId)
.Skip($"{Capitalize(_provider)} connector does not support rate limit info")
.Build();
}
try
{
var rateLimitInfo = await rateLimitConnector.GetRateLimitInfoAsync(ct);
var evidence = context.CreateEvidence()
.Add("Integration", _integration.Name)
.Add("Limit", rateLimitInfo.Limit.ToString(CultureInfo.InvariantCulture))
.Add("Remaining", rateLimitInfo.Remaining.ToString(CultureInfo.InvariantCulture))
.Add("ResetsAt", rateLimitInfo.ResetsAt.ToString("O", CultureInfo.InvariantCulture))
.Add("UsedPercent", $"{(rateLimitInfo.Limit - rateLimitInfo.Remaining) * 100.0 / rateLimitInfo.Limit:F1}%")
.Build("Rate limit status");
if (rateLimitInfo.Remaining == 0)
{
var resetsIn = rateLimitInfo.ResetsAt - context.TimeProvider.GetUtcNow();
return context.CreateResult(CheckId)
.Fail($"Rate limit exhausted - resets in {resetsIn.TotalMinutes:F0} minutes")
.WithEvidence(evidence)
.WithCauses(
"Too many API requests",
"CI/CD jobs consuming quota",
"Webhook flood")
.WithRemediation(rb => rb
.AddStep(1, "Wait for rate limit reset",
$"# Rate limit resets at {rateLimitInfo.ResetsAt:HH:mm:ss} UTC",
CommandType.Manual)
.AddStep(2, "Check for excessive API usage",
"stella integrations usage --integration " + _integration.Name,
CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
if (rateLimitInfo.Remaining < WarningThreshold)
{
return context.CreateResult(CheckId)
.Warn($"Rate limit low: {rateLimitInfo.Remaining}/{rateLimitInfo.Limit} remaining")
.WithEvidence(evidence)
.WithCauses("High API usage rate")
.Build();
}
return context.CreateResult(CheckId)
.Pass($"Rate limit OK: {rateLimitInfo.Remaining}/{rateLimitInfo.Limit} remaining")
.WithEvidence(evidence)
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Warn($"Could not check rate limit: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
}
private static string Capitalize(string s) =>
string.IsNullOrEmpty(s) ? s : char.ToUpper(s[0], CultureInfo.InvariantCulture) + s[1..];
}
```
---
### Task 5: Registry Plugin Structure
**Status:** TODO
```
StellaOps.Doctor.Plugin.Registry/
├── RegistryDoctorPlugin.cs
├── Checks/
│ ├── RegistryConnectivityCheck.cs
│ ├── RegistryAuthCheck.cs
│ └── RegistryPullCheck.cs
├── Providers/
│ ├── HarborCheckProvider.cs
│ └── EcrCheckProvider.cs
└── StellaOps.Doctor.Plugin.Registry.csproj
```
---
### Task 6: check.integration.registry.harbor.connectivity
**Status:** TODO
---
### Task 7: check.integration.registry.harbor.pull
**Status:** TODO
```csharp
public sealed class RegistryPullCheck : IDoctorCheck
{
private readonly Integration _integration;
private readonly string _provider;
public RegistryPullCheck(Integration integration, string provider)
{
_integration = integration;
_provider = provider;
}
public string CheckId => $"check.integration.registry.{_provider}.pull";
public string Name => $"{Capitalize(_provider)} Pull Access";
public string Description => $"Verify can pull images from {Capitalize(_provider)}";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["integration", "registry"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
public bool CanRun(DoctorPluginContext context) => true;
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var connectorFactory = context.Services.GetRequiredService<IConnectorFactory>();
var connector = await connectorFactory.CreateAsync(_integration, ct);
if (connector is not IRegistryConnectorCapability registryConnector)
{
return context.CreateResult(CheckId)
.Skip("Integration is not a registry connector")
.Build();
}
try
{
// Get test repository from config or use library
var testRepo = _integration.Config.TryGetProperty("testRepository", out var tr)
? tr.GetString()
: "library/alpine";
var canPull = await registryConnector.CanPullAsync(testRepo!, ct);
if (canPull)
{
return context.CreateResult(CheckId)
.Pass($"Pull access verified for {testRepo}")
.WithEvidence(eb => eb
.Add("Integration", _integration.Name)
.Add("TestRepository", testRepo!))
.Build();
}
return context.CreateResult(CheckId)
.Warn($"Cannot pull from {testRepo}")
.WithEvidence(eb => eb
.Add("Integration", _integration.Name)
.Add("TestRepository", testRepo!))
.WithCauses(
"Insufficient permissions",
"Repository does not exist",
"Private repository without access")
.WithRemediation(rb => rb
.AddStep(1, "Test pull manually",
$"docker pull {_integration.Config.GetProperty("host").GetString()}/{testRepo}",
CommandType.Shell)
.AddStep(2, "Check repository permissions",
"# Verify user has pull access in registry UI", CommandType.Manual))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Fail($"Pull check failed: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
}
private static string Capitalize(string s) =>
string.IsNullOrEmpty(s) ? s : char.ToUpper(s[0], CultureInfo.InvariantCulture) + s[1..];
}
```
---
### Task 8: Test Suite
**Status:** TODO
```
src/Doctor/__Tests/
├── StellaOps.Doctor.Plugin.Scm.Tests/
│ └── Checks/
│ ├── ScmConnectivityCheckTests.cs
│ └── ScmRateLimitCheckTests.cs
└── StellaOps.Doctor.Plugin.Registry.Tests/
└── Checks/
└── RegistryPullCheckTests.cs
```
---
## Dependencies
| Dependency | Package/Module | Status |
|------------|----------------|--------|
| IIntegrationManager | ReleaseOrchestrator.IntegrationHub | EXISTS |
| IConnectorFactory | ReleaseOrchestrator.IntegrationHub | EXISTS |
| IRateLimitInfo | ReleaseOrchestrator.IntegrationHub | EXISTS |
| IRegistryConnectorCapability | ReleaseOrchestrator.Plugin | EXISTS |
---
## Acceptance Criteria (Sprint)
- [ ] SCM plugin with 8 checks (GitHub, GitLab)
- [ ] Registry plugin with 6 checks (Harbor, ECR)
- [ ] Integration with existing connector infrastructure
- [ ] Dynamic check generation based on configured integrations
- [ ] Test coverage >= 85%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,591 @@
# SPRINT: CLI Doctor Command Implementation
> **Implementation ID:** 20260112
> **Sprint ID:** 001_006
> **Module:** CLI
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_002 (Core Plugin)
---
## Overview
Implement the `stella doctor` CLI command that provides comprehensive self-service diagnostics from the terminal. This is the primary interface for operators to diagnose and fix issues.
---
## Working Directory
```
src/Cli/StellaOps.Cli/Commands/
```
---
## Command Specification
### Usage
```bash
stella doctor [options]
```
### Options
| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--format` | `-f` | enum | `text` | Output format: `text`, `json`, `markdown` |
| `--quick` | `-q` | flag | false | Run only quick checks (tagged `quick`) |
| `--full` | | flag | false | Run all checks including slow/intensive |
| `--category` | `-c` | string[] | all | Filter by category |
| `--plugin` | `-p` | string[] | all | Filter by plugin ID |
| `--check` | | string | | Run single check by ID |
| `--severity` | `-s` | enum[] | all | Filter output by severity |
| `--export` | `-e` | path | | Export report to file |
| `--timeout` | `-t` | duration | 30s | Per-check timeout |
| `--parallel` | | int | 4 | Max parallel check execution |
| `--no-remediation` | | flag | false | Skip remediation output |
| `--verbose` | `-v` | flag | false | Include detailed evidence |
| `--tenant` | | string | | Tenant context |
| `--list-checks` | | flag | false | List available checks |
| `--list-plugins` | | flag | false | List available plugins |
### Exit Codes
| Code | Meaning |
|------|---------|
| 0 | All checks passed |
| 1 | One or more warnings |
| 2 | One or more failures |
| 3 | Doctor engine error |
| 4 | Invalid arguments |
| 5 | Timeout exceeded |
---
## Deliverables
### Task 1: Command Group Structure
**Status:** TODO
```
src/Cli/StellaOps.Cli/
├── Commands/
│ └── DoctorCommandGroup.cs
├── Handlers/
│ └── DoctorCommandHandlers.cs
└── Output/
└── DoctorOutputRenderer.cs
```
**DoctorCommandGroup:**
```csharp
public sealed class DoctorCommandGroup : ICommandGroup
{
public Command Create()
{
var command = new Command("doctor", "Run diagnostic checks on the Stella Ops deployment");
// Format option
var formatOption = new Option<OutputFormat>(
aliases: ["--format", "-f"],
description: "Output format: text, json, markdown",
getDefaultValue: () => OutputFormat.Text);
command.AddOption(formatOption);
// Mode options
var quickOption = new Option<bool>(
"--quick",
"Run only quick checks");
quickOption.AddAlias("-q");
command.AddOption(quickOption);
var fullOption = new Option<bool>(
"--full",
"Run all checks including slow/intensive");
command.AddOption(fullOption);
// Filter options
var categoryOption = new Option<string[]>(
aliases: ["--category", "-c"],
description: "Filter by category (core, database, servicegraph, integration, security, observability)");
command.AddOption(categoryOption);
var pluginOption = new Option<string[]>(
aliases: ["--plugin", "-p"],
description: "Filter by plugin ID");
command.AddOption(pluginOption);
var checkOption = new Option<string>(
"--check",
"Run single check by ID");
command.AddOption(checkOption);
var severityOption = new Option<DoctorSeverity[]>(
aliases: ["--severity", "-s"],
description: "Filter output by severity (pass, info, warn, fail)");
command.AddOption(severityOption);
// Output options
var exportOption = new Option<FileInfo?>(
aliases: ["--export", "-e"],
description: "Export report to file");
command.AddOption(exportOption);
var verboseOption = new Option<bool>(
aliases: ["--verbose", "-v"],
description: "Include detailed evidence in output");
command.AddOption(verboseOption);
var noRemediationOption = new Option<bool>(
"--no-remediation",
"Skip remediation command generation");
command.AddOption(noRemediationOption);
// Execution options
var timeoutOption = new Option<TimeSpan>(
aliases: ["--timeout", "-t"],
description: "Per-check timeout",
getDefaultValue: () => TimeSpan.FromSeconds(30));
command.AddOption(timeoutOption);
var parallelOption = new Option<int>(
"--parallel",
getDefaultValue: () => 4,
description: "Max parallel check execution");
command.AddOption(parallelOption);
var tenantOption = new Option<string?>(
"--tenant",
"Tenant context for multi-tenant checks");
command.AddOption(tenantOption);
// List options
var listChecksOption = new Option<bool>(
"--list-checks",
"List available checks and exit");
command.AddOption(listChecksOption);
var listPluginsOption = new Option<bool>(
"--list-plugins",
"List available plugins and exit");
command.AddOption(listPluginsOption);
command.SetHandler(DoctorCommandHandlers.RunAsync);
return command;
}
}
```
---
### Task 2: Command Handler
**Status:** TODO
```csharp
public static class DoctorCommandHandlers
{
public static async Task<int> RunAsync(InvocationContext context)
{
var ct = context.GetCancellationToken();
var services = context.GetRequiredService<IServiceProvider>();
var console = context.Console;
// Parse options
var format = context.ParseResult.GetValueForOption<OutputFormat>("--format");
var quick = context.ParseResult.GetValueForOption<bool>("--quick");
var full = context.ParseResult.GetValueForOption<bool>("--full");
var categories = context.ParseResult.GetValueForOption<string[]>("--category");
var plugins = context.ParseResult.GetValueForOption<string[]>("--plugin");
var checkId = context.ParseResult.GetValueForOption<string>("--check");
var severities = context.ParseResult.GetValueForOption<DoctorSeverity[]>("--severity");
var exportPath = context.ParseResult.GetValueForOption<FileInfo?>("--export");
var verbose = context.ParseResult.GetValueForOption<bool>("--verbose");
var noRemediation = context.ParseResult.GetValueForOption<bool>("--no-remediation");
var timeout = context.ParseResult.GetValueForOption<TimeSpan>("--timeout");
var parallel = context.ParseResult.GetValueForOption<int>("--parallel");
var tenant = context.ParseResult.GetValueForOption<string?>("--tenant");
var listChecks = context.ParseResult.GetValueForOption<bool>("--list-checks");
var listPlugins = context.ParseResult.GetValueForOption<bool>("--list-plugins");
var engine = services.GetRequiredService<DoctorEngine>();
var renderer = services.GetRequiredService<DoctorOutputRenderer>();
// Handle list operations
if (listPlugins)
{
var pluginList = engine.ListPlugins();
renderer.RenderPluginList(console, pluginList, format);
return CliExitCodes.Success;
}
if (listChecks)
{
var checkList = engine.ListChecks(new DoctorRunOptions
{
Categories = categories?.ToImmutableArray(),
Plugins = plugins?.ToImmutableArray()
});
renderer.RenderCheckList(console, checkList, format);
return CliExitCodes.Success;
}
// Build run options
var runMode = quick ? DoctorRunMode.Quick :
full ? DoctorRunMode.Full :
DoctorRunMode.Normal;
var options = new DoctorRunOptions
{
Mode = runMode,
Categories = categories?.ToImmutableArray(),
Plugins = plugins?.ToImmutableArray(),
CheckIds = string.IsNullOrEmpty(checkId) ? null : [checkId],
Timeout = timeout,
Parallelism = parallel,
IncludeRemediation = !noRemediation,
TenantId = tenant
};
// Run doctor with progress
var progress = new Progress<DoctorCheckProgress>(p =>
{
if (format == OutputFormat.Text)
{
renderer.RenderProgress(console, p);
}
});
try
{
var report = await engine.RunAsync(options, progress, ct);
// Filter by severity if requested
var filteredReport = severities?.Length > 0
? FilterReportBySeverity(report, severities)
: report;
// Render output
var formatOptions = new ReportFormatOptions
{
Verbose = verbose,
IncludeRemediation = !noRemediation,
SeverityFilter = severities?.ToImmutableArray()
};
renderer.RenderReport(console, filteredReport, format, formatOptions);
// Export if requested
if (exportPath is not null)
{
await ExportReportAsync(filteredReport, exportPath, format, formatOptions, ct);
console.WriteLine($"Report exported to: {exportPath.FullName}");
}
// Return appropriate exit code
return report.OverallSeverity switch
{
DoctorSeverity.Pass => CliExitCodes.Success,
DoctorSeverity.Info => CliExitCodes.Success,
DoctorSeverity.Warn => CliExitCodes.DoctorWarnings,
DoctorSeverity.Fail => CliExitCodes.DoctorFailures,
_ => CliExitCodes.Success
};
}
catch (OperationCanceledException)
{
console.Error.WriteLine("Doctor run cancelled");
return CliExitCodes.DoctorTimeout;
}
catch (Exception ex)
{
console.Error.WriteLine($"Doctor engine error: {ex.Message}");
return CliExitCodes.DoctorEngineError;
}
}
private static DoctorReport FilterReportBySeverity(
DoctorReport report,
DoctorSeverity[] severities)
{
var severitySet = severities.ToHashSet();
return report with
{
Results = report.Results
.Where(r => severitySet.Contains(r.Severity))
.ToImmutableArray()
};
}
private static async Task ExportReportAsync(
DoctorReport report,
FileInfo exportPath,
OutputFormat format,
ReportFormatOptions options,
CancellationToken ct)
{
var formatter = format switch
{
OutputFormat.Json => new JsonReportFormatter(),
OutputFormat.Markdown => new MarkdownReportFormatter(),
_ => new TextReportFormatter()
};
var content = formatter.FormatReport(report, options);
await File.WriteAllTextAsync(exportPath.FullName, content, ct);
}
}
public enum OutputFormat
{
Text,
Json,
Markdown
}
```
---
### Task 3: Output Renderer
**Status:** TODO
```csharp
public sealed class DoctorOutputRenderer
{
private readonly IAnsiConsole _console;
public DoctorOutputRenderer(IAnsiConsole console)
{
_console = console;
}
public void RenderProgress(IConsole console, DoctorCheckProgress progress)
{
// Clear previous line and show progress
console.Write($"\r[{progress.Completed}/{progress.Total}] {progress.CheckId}...".PadRight(80));
}
public void RenderReport(
IConsole console,
DoctorReport report,
OutputFormat format,
ReportFormatOptions options)
{
var formatter = GetFormatter(format);
var output = formatter.FormatReport(report, options);
console.WriteLine(output);
}
public void RenderPluginList(
IConsole console,
IReadOnlyList<DoctorPluginMetadata> plugins,
OutputFormat format)
{
if (format == OutputFormat.Json)
{
var json = JsonSerializer.Serialize(plugins, JsonSerializerOptions.Default);
console.WriteLine(json);
return;
}
console.WriteLine("Available Doctor Plugins");
console.WriteLine("========================");
console.WriteLine();
foreach (var plugin in plugins)
{
console.WriteLine($" {plugin.PluginId}");
console.WriteLine($" Name: {plugin.DisplayName}");
console.WriteLine($" Category: {plugin.Category}");
console.WriteLine($" Version: {plugin.Version}");
console.WriteLine($" Checks: {plugin.CheckCount}");
console.WriteLine();
}
}
public void RenderCheckList(
IConsole console,
IReadOnlyList<DoctorCheckMetadata> checks,
OutputFormat format)
{
if (format == OutputFormat.Json)
{
var json = JsonSerializer.Serialize(checks, JsonSerializerOptions.Default);
console.WriteLine(json);
return;
}
console.WriteLine($"Available Checks ({checks.Count})");
console.WriteLine("=".PadRight(50, '='));
console.WriteLine();
var byCategory = checks.GroupBy(c => c.Category);
foreach (var group in byCategory.OrderBy(g => g.Key))
{
console.WriteLine($"[{group.Key}]");
foreach (var check in group.OrderBy(c => c.CheckId))
{
var tags = string.Join(", ", check.Tags);
console.WriteLine($" {check.CheckId}");
console.WriteLine($" {check.Description}");
console.WriteLine($" Tags: {tags}");
console.WriteLine();
}
}
}
private static IReportFormatter GetFormatter(OutputFormat format) => format switch
{
OutputFormat.Json => new JsonReportFormatter(),
OutputFormat.Markdown => new MarkdownReportFormatter(),
_ => new TextReportFormatter()
};
}
```
---
### Task 4: Exit Codes Registration
**Status:** TODO
Add to `CliExitCodes.cs`:
```csharp
public static class CliExitCodes
{
// Existing codes...
// Doctor exit codes (10-19)
public const int DoctorWarnings = 10;
public const int DoctorFailures = 11;
public const int DoctorEngineError = 12;
public const int DoctorTimeout = 13;
public const int DoctorInvalidArgs = 14;
}
```
---
### Task 5: DI Registration
**Status:** TODO
Register in CLI startup:
```csharp
// In Program.cs or CliBootstrapper.cs
services.AddDoctor();
services.AddDoctorPlugin<CoreDoctorPlugin>();
services.AddDoctorPlugin<DatabaseDoctorPlugin>();
services.AddDoctorPlugin<ServiceGraphDoctorPlugin>();
services.AddDoctorPlugin<SecurityDoctorPlugin>();
services.AddDoctorPlugin<ScmDoctorPlugin>();
services.AddDoctorPlugin<RegistryDoctorPlugin>();
services.AddSingleton<DoctorOutputRenderer>();
```
---
### Task 6: Test Suite
**Status:** TODO
```
src/Cli/__Tests/StellaOps.Cli.Tests/Commands/
├── DoctorCommandGroupTests.cs
├── DoctorCommandHandlersTests.cs
└── DoctorOutputRendererTests.cs
```
**Test Scenarios:**
1. **Command Parsing**
- All options parse correctly
- Conflicting options handled (--quick vs --full)
- Invalid values rejected
2. **Execution**
- Quick mode runs only quick-tagged checks
- Full mode runs all checks
- Single check by ID works
- Category filtering works
3. **Output**
- Text format is human-readable
- JSON format is valid JSON
- Markdown format is valid markdown
- Export creates file with correct content
4. **Exit Codes**
- Returns 0 for all pass
- Returns 1 for warnings
- Returns 2 for failures
---
## Usage Examples
```bash
# Quick health check (default)
stella doctor
# Full diagnostic
stella doctor --full
# Check only database
stella doctor --category database
# Check specific integration
stella doctor --plugin scm.github
# Run single check
stella doctor --check check.database.migrations.pending
# JSON output for CI/CD
stella doctor --format json --severity fail,warn
# Export markdown report
stella doctor --full --format markdown --export doctor-report.md
# Verbose with all evidence
stella doctor --verbose --full
# List available checks
stella doctor --list-checks
# List available plugins
stella doctor --list-plugins
# Quick check with 60s timeout
stella doctor --quick --timeout 60s
```
---
## Acceptance Criteria (Sprint)
- [ ] All command options implemented
- [ ] Text output matches specification
- [ ] JSON output is valid and complete
- [ ] Markdown output suitable for tickets
- [ ] Exit codes follow specification
- [ ] Progress display during execution
- [ ] Export to file works
- [ ] Test coverage >= 85%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,585 @@
# SPRINT: Doctor API Endpoints
> **Implementation ID:** 20260112
> **Sprint ID:** 001_007
> **Module:** BE (Backend)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_002 (Core Plugin)
---
## Overview
Implement REST API endpoints for the Doctor system, enabling programmatic access for CI/CD pipelines, monitoring systems, and the web UI.
---
## Working Directory
```
src/Doctor/StellaOps.Doctor.WebService/
```
---
## API Specification
### Base Path
```
/api/v1/doctor
```
### Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/checks` | List available checks |
| `GET` | `/plugins` | List available plugins |
| `POST` | `/run` | Execute doctor checks |
| `GET` | `/run/{runId}` | Get run results |
| `GET` | `/run/{runId}/stream` | SSE stream for progress |
| `GET` | `/reports` | List historical reports |
| `GET` | `/reports/{reportId}` | Get specific report |
| `DELETE` | `/reports/{reportId}` | Delete report |
---
## Deliverables
### Task 1: Project Structure
**Status:** TODO
```
StellaOps.Doctor.WebService/
├── Endpoints/
│ ├── DoctorEndpoints.cs
│ ├── ChecksEndpoints.cs
│ ├── PluginsEndpoints.cs
│ ├── RunEndpoints.cs
│ └── ReportsEndpoints.cs
├── Models/
│ ├── RunDoctorRequest.cs
│ ├── RunDoctorResponse.cs
│ ├── CheckListResponse.cs
│ ├── PluginListResponse.cs
│ └── ReportListResponse.cs
├── Services/
│ ├── DoctorRunService.cs
│ └── ReportStorageService.cs
├── Program.cs
└── StellaOps.Doctor.WebService.csproj
```
---
### Task 2: Endpoint Registration
**Status:** TODO
```csharp
public static class DoctorEndpoints
{
public static void MapDoctorEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/v1/doctor")
.WithTags("Doctor")
.RequireAuthorization("doctor:run");
// Checks
group.MapGet("/checks", ChecksEndpoints.ListChecks)
.WithName("ListDoctorChecks")
.WithSummary("List available diagnostic checks");
// Plugins
group.MapGet("/plugins", PluginsEndpoints.ListPlugins)
.WithName("ListDoctorPlugins")
.WithSummary("List available doctor plugins");
// Run
group.MapPost("/run", RunEndpoints.StartRun)
.WithName("StartDoctorRun")
.WithSummary("Start a doctor diagnostic run");
group.MapGet("/run/{runId}", RunEndpoints.GetRunResult)
.WithName("GetDoctorRunResult")
.WithSummary("Get results of a doctor run");
group.MapGet("/run/{runId}/stream", RunEndpoints.StreamRunProgress)
.WithName("StreamDoctorRunProgress")
.WithSummary("Stream real-time progress of a doctor run");
// Reports
group.MapGet("/reports", ReportsEndpoints.ListReports)
.WithName("ListDoctorReports")
.WithSummary("List historical doctor reports");
group.MapGet("/reports/{reportId}", ReportsEndpoints.GetReport)
.WithName("GetDoctorReport")
.WithSummary("Get a specific doctor report");
group.MapDelete("/reports/{reportId}", ReportsEndpoints.DeleteReport)
.WithName("DeleteDoctorReport")
.WithSummary("Delete a doctor report")
.RequireAuthorization("doctor:admin");
}
}
```
---
### Task 3: List Checks Endpoint
**Status:** TODO
```csharp
public static class ChecksEndpoints
{
public static async Task<IResult> ListChecks(
[FromQuery] string? category,
[FromQuery] string? tags,
[FromQuery] string? plugin,
[FromServices] DoctorEngine engine)
{
var options = new DoctorRunOptions
{
Categories = string.IsNullOrEmpty(category) ? null : [category],
Plugins = string.IsNullOrEmpty(plugin) ? null : [plugin],
Tags = string.IsNullOrEmpty(tags) ? null : tags.Split(',').ToImmutableArray()
};
var checks = engine.ListChecks(options);
var response = new CheckListResponse
{
Checks = checks.Select(c => new CheckMetadataDto
{
CheckId = c.CheckId,
Name = c.Name,
Description = c.Description,
PluginId = c.PluginId,
Category = c.Category,
DefaultSeverity = c.DefaultSeverity.ToString().ToLowerInvariant(),
Tags = c.Tags,
EstimatedDurationMs = (int)c.EstimatedDuration.TotalMilliseconds
}).ToImmutableArray(),
Total = checks.Count
};
return Results.Ok(response);
}
}
public sealed record CheckListResponse
{
public required IReadOnlyList<CheckMetadataDto> Checks { get; init; }
public required int Total { get; init; }
}
public sealed record CheckMetadataDto
{
public required string CheckId { get; init; }
public required string Name { get; init; }
public required string Description { get; init; }
public string? PluginId { get; init; }
public string? Category { get; init; }
public required string DefaultSeverity { get; init; }
public required IReadOnlyList<string> Tags { get; init; }
public int EstimatedDurationMs { get; init; }
}
```
---
### Task 4: Run Endpoint
**Status:** TODO
```csharp
public static class RunEndpoints
{
private static readonly ConcurrentDictionary<string, DoctorRunState> _runs = new();
public static async Task<IResult> StartRun(
[FromBody] RunDoctorRequest request,
[FromServices] DoctorEngine engine,
[FromServices] DoctorRunService runService,
CancellationToken ct)
{
var runId = await runService.StartRunAsync(request, ct);
return Results.Accepted(
$"/api/v1/doctor/run/{runId}",
new RunStartedResponse
{
RunId = runId,
Status = "running",
StartedAt = DateTimeOffset.UtcNow,
ChecksTotal = request.CheckIds?.Count ?? 0
});
}
public static async Task<IResult> GetRunResult(
string runId,
[FromServices] DoctorRunService runService,
CancellationToken ct)
{
var result = await runService.GetRunResultAsync(runId, ct);
if (result is null)
return Results.NotFound(new { error = "Run not found", runId });
return Results.Ok(result);
}
public static async Task StreamRunProgress(
string runId,
HttpContext context,
[FromServices] DoctorRunService runService,
CancellationToken ct)
{
context.Response.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
context.Response.Headers.Connection = "keep-alive";
await foreach (var progress in runService.StreamProgressAsync(runId, ct))
{
var json = JsonSerializer.Serialize(progress);
await context.Response.WriteAsync($"event: {progress.EventType}\n", ct);
await context.Response.WriteAsync($"data: {json}\n\n", ct);
await context.Response.Body.FlushAsync(ct);
}
}
}
public sealed record RunDoctorRequest
{
public string Mode { get; init; } = "quick"; // quick, normal, full
public IReadOnlyList<string>? Categories { get; init; }
public IReadOnlyList<string>? Plugins { get; init; }
public IReadOnlyList<string>? CheckIds { get; init; }
public int TimeoutMs { get; init; } = 30000;
public int Parallelism { get; init; } = 4;
public bool IncludeRemediation { get; init; } = true;
public string? TenantId { get; init; }
}
public sealed record RunStartedResponse
{
public required string RunId { get; init; }
public required string Status { get; init; }
public required DateTimeOffset StartedAt { get; init; }
public int ChecksTotal { get; init; }
}
```
---
### Task 5: Run Service
**Status:** TODO
```csharp
public sealed class DoctorRunService
{
private readonly DoctorEngine _engine;
private readonly IReportStorageService _storage;
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary<string, DoctorRunState> _activeRuns = new();
public DoctorRunService(
DoctorEngine engine,
IReportStorageService storage,
TimeProvider timeProvider)
{
_engine = engine;
_storage = storage;
_timeProvider = timeProvider;
}
public async Task<string> StartRunAsync(RunDoctorRequest request, CancellationToken ct)
{
var runMode = Enum.Parse<DoctorRunMode>(request.Mode, ignoreCase: true);
var options = new DoctorRunOptions
{
Mode = runMode,
Categories = request.Categories?.ToImmutableArray(),
Plugins = request.Plugins?.ToImmutableArray(),
CheckIds = request.CheckIds?.ToImmutableArray(),
Timeout = TimeSpan.FromMilliseconds(request.TimeoutMs),
Parallelism = request.Parallelism,
IncludeRemediation = request.IncludeRemediation,
TenantId = request.TenantId
};
var runId = GenerateRunId();
var state = new DoctorRunState
{
RunId = runId,
Status = "running",
StartedAt = _timeProvider.GetUtcNow(),
Progress = Channel.CreateUnbounded<DoctorProgressEvent>()
};
_activeRuns[runId] = state;
// Run in background
_ = Task.Run(async () =>
{
try
{
var progress = new Progress<DoctorCheckProgress>(p =>
{
state.Progress.Writer.TryWrite(new DoctorProgressEvent
{
EventType = "check-completed",
CheckId = p.CheckId,
Severity = p.Severity.ToString().ToLowerInvariant(),
Completed = p.Completed,
Total = p.Total
});
});
var report = await _engine.RunAsync(options, progress, ct);
state.Report = report;
state.Status = "completed";
state.CompletedAt = _timeProvider.GetUtcNow();
state.Progress.Writer.TryWrite(new DoctorProgressEvent
{
EventType = "run-completed",
RunId = runId,
Summary = new
{
passed = report.Summary.Passed,
warnings = report.Summary.Warnings,
failed = report.Summary.Failed
}
});
state.Progress.Writer.Complete();
// Store report
await _storage.StoreReportAsync(report, ct);
}
catch (Exception ex)
{
state.Status = "failed";
state.Error = ex.Message;
state.Progress.Writer.TryComplete(ex);
}
}, ct);
return runId;
}
public async Task<DoctorRunResultResponse?> GetRunResultAsync(string runId, CancellationToken ct)
{
if (_activeRuns.TryGetValue(runId, out var state))
{
if (state.Report is null)
{
return new DoctorRunResultResponse
{
RunId = runId,
Status = state.Status,
StartedAt = state.StartedAt,
Error = state.Error
};
}
return MapToResponse(state.Report);
}
// Try to load from storage
var report = await _storage.GetReportAsync(runId, ct);
return report is null ? null : MapToResponse(report);
}
public async IAsyncEnumerable<DoctorProgressEvent> StreamProgressAsync(
string runId,
[EnumeratorCancellation] CancellationToken ct)
{
if (!_activeRuns.TryGetValue(runId, out var state))
yield break;
await foreach (var progress in state.Progress.Reader.ReadAllAsync(ct))
{
yield return progress;
}
}
private string GenerateRunId()
{
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
var suffix = Guid.NewGuid().ToString("N")[..6];
return $"dr_{timestamp}_{suffix}";
}
private static DoctorRunResultResponse MapToResponse(DoctorReport report) => new()
{
RunId = report.RunId,
Status = "completed",
StartedAt = report.StartedAt,
CompletedAt = report.CompletedAt,
DurationMs = (long)report.Duration.TotalMilliseconds,
Summary = new DoctorSummaryDto
{
Passed = report.Summary.Passed,
Info = report.Summary.Info,
Warnings = report.Summary.Warnings,
Failed = report.Summary.Failed,
Skipped = report.Summary.Skipped,
Total = report.Summary.Total
},
OverallSeverity = report.OverallSeverity.ToString().ToLowerInvariant(),
Results = report.Results.Select(MapCheckResult).ToImmutableArray()
};
private static DoctorCheckResultDto MapCheckResult(DoctorCheckResult result) => new()
{
CheckId = result.CheckId,
PluginId = result.PluginId,
Category = result.Category,
Severity = result.Severity.ToString().ToLowerInvariant(),
Diagnosis = result.Diagnosis,
Evidence = new EvidenceDto
{
Description = result.Evidence.Description,
Data = result.Evidence.Data
},
LikelyCauses = result.LikelyCauses,
Remediation = result.Remediation is null ? null : new RemediationDto
{
RequiresBackup = result.Remediation.RequiresBackup,
SafetyNote = result.Remediation.SafetyNote,
Steps = result.Remediation.Steps.Select(s => new RemediationStepDto
{
Order = s.Order,
Description = s.Description,
Command = s.Command,
CommandType = s.CommandType.ToString().ToLowerInvariant()
}).ToImmutableArray()
},
VerificationCommand = result.VerificationCommand,
DurationMs = (int)result.Duration.TotalMilliseconds,
ExecutedAt = result.ExecutedAt
};
}
internal sealed class DoctorRunState
{
public required string RunId { get; init; }
public required string Status { get; set; }
public required DateTimeOffset StartedAt { get; init; }
public DateTimeOffset? CompletedAt { get; set; }
public DoctorReport? Report { get; set; }
public string? Error { get; set; }
public required Channel<DoctorProgressEvent> Progress { get; init; }
}
public sealed record DoctorProgressEvent
{
public required string EventType { get; init; }
public string? RunId { get; init; }
public string? CheckId { get; init; }
public string? Severity { get; init; }
public int? Completed { get; init; }
public int? Total { get; init; }
public object? Summary { get; init; }
}
```
---
### Task 6: Report Storage Service
**Status:** TODO
```csharp
public interface IReportStorageService
{
Task StoreReportAsync(DoctorReport report, CancellationToken ct);
Task<DoctorReport?> GetReportAsync(string runId, CancellationToken ct);
Task<IReadOnlyList<DoctorReportSummary>> ListReportsAsync(int limit, int offset, CancellationToken ct);
Task DeleteReportAsync(string runId, CancellationToken ct);
}
public sealed class PostgresReportStorageService : IReportStorageService
{
private readonly NpgsqlDataSource _dataSource;
public PostgresReportStorageService(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
}
public async Task StoreReportAsync(DoctorReport report, CancellationToken ct)
{
await using var connection = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = connection.CreateCommand();
cmd.CommandText = @"
INSERT INTO doctor.reports (run_id, started_at, completed_at, duration_ms, overall_severity, summary_json, results_json)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (run_id) DO UPDATE SET
completed_at = EXCLUDED.completed_at,
duration_ms = EXCLUDED.duration_ms,
overall_severity = EXCLUDED.overall_severity,
summary_json = EXCLUDED.summary_json,
results_json = EXCLUDED.results_json";
cmd.Parameters.AddWithValue(report.RunId);
cmd.Parameters.AddWithValue(report.StartedAt);
cmd.Parameters.AddWithValue(report.CompletedAt);
cmd.Parameters.AddWithValue((long)report.Duration.TotalMilliseconds);
cmd.Parameters.AddWithValue(report.OverallSeverity.ToString());
cmd.Parameters.AddWithValue(JsonSerializer.Serialize(report.Summary));
cmd.Parameters.AddWithValue(JsonSerializer.Serialize(report.Results));
await cmd.ExecuteNonQueryAsync(ct);
}
// Additional methods...
}
```
---
### Task 7: Test Suite
**Status:** TODO
```
src/Doctor/__Tests/StellaOps.Doctor.WebService.Tests/
├── Endpoints/
│ ├── ChecksEndpointsTests.cs
│ ├── RunEndpointsTests.cs
│ └── ReportsEndpointsTests.cs
└── Services/
├── DoctorRunServiceTests.cs
└── ReportStorageServiceTests.cs
```
---
## Acceptance Criteria (Sprint)
- [ ] All endpoints implemented
- [ ] SSE streaming for progress
- [ ] Report storage in PostgreSQL
- [ ] OpenAPI documentation
- [ ] Authorization on endpoints
- [ ] Test coverage >= 85%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,733 @@
# SPRINT: Doctor Dashboard - Angular UI Implementation
> **Implementation ID:** 20260112
> **Sprint ID:** 001_008
> **Module:** FE (Frontend)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_007 (API Endpoints)
---
## Overview
Implement the Doctor Dashboard in the Angular web application, providing an interactive UI for running diagnostics, viewing results, and executing remediation commands.
---
## Working Directory
```
src/Web/StellaOps.Web/src/app/features/doctor/
```
---
## Route
```
/ops/doctor
```
---
## Deliverables
### Task 1: Feature Module Structure
**Status:** TODO
```
src/app/features/doctor/
├── doctor.routes.ts
├── doctor-dashboard.component.ts
├── doctor-dashboard.component.html
├── doctor-dashboard.component.scss
├── components/
│ ├── check-list/
│ │ ├── check-list.component.ts
│ │ ├── check-list.component.html
│ │ └── check-list.component.scss
│ ├── check-result/
│ │ ├── check-result.component.ts
│ │ ├── check-result.component.html
│ │ └── check-result.component.scss
│ ├── remediation-panel/
│ │ ├── remediation-panel.component.ts
│ │ ├── remediation-panel.component.html
│ │ └── remediation-panel.component.scss
│ ├── evidence-viewer/
│ │ ├── evidence-viewer.component.ts
│ │ └── evidence-viewer.component.html
│ ├── summary-strip/
│ │ ├── summary-strip.component.ts
│ │ └── summary-strip.component.html
│ └── export-dialog/
│ ├── export-dialog.component.ts
│ └── export-dialog.component.html
├── services/
│ ├── doctor.client.ts
│ ├── doctor.service.ts
│ └── doctor.store.ts
└── models/
├── check-result.model.ts
├── doctor-report.model.ts
└── remediation.model.ts
```
---
### Task 2: Routes Configuration
**Status:** TODO
```typescript
// doctor.routes.ts
import { Routes } from '@angular/router';
export const DOCTOR_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./doctor-dashboard.component').then(m => m.DoctorDashboardComponent),
title: 'Doctor Diagnostics',
data: {
requiredScopes: ['doctor:run']
}
}
];
```
Register in main routes:
```typescript
// app.routes.ts
{
path: 'ops/doctor',
loadChildren: () => import('./features/doctor/doctor.routes').then(m => m.DOCTOR_ROUTES),
canActivate: [authGuard]
}
```
---
### Task 3: API Client
**Status:** TODO
```typescript
// services/doctor.client.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '@env/environment';
export interface CheckMetadata {
checkId: string;
name: string;
description: string;
pluginId: string;
category: string;
defaultSeverity: string;
tags: string[];
estimatedDurationMs: number;
}
export interface RunDoctorRequest {
mode: 'quick' | 'normal' | 'full';
categories?: string[];
plugins?: string[];
checkIds?: string[];
timeoutMs?: number;
parallelism?: number;
includeRemediation?: boolean;
}
export interface DoctorReport {
runId: string;
status: string;
startedAt: string;
completedAt?: string;
durationMs?: number;
summary: DoctorSummary;
overallSeverity: string;
results: CheckResult[];
}
export interface DoctorSummary {
passed: number;
info: number;
warnings: number;
failed: number;
skipped: number;
total: number;
}
export interface CheckResult {
checkId: string;
pluginId: string;
category: string;
severity: string;
diagnosis: string;
evidence: Evidence;
likelyCauses?: string[];
remediation?: Remediation;
verificationCommand?: string;
durationMs: number;
executedAt: string;
}
export interface Evidence {
description: string;
data: Record<string, string>;
}
export interface Remediation {
requiresBackup: boolean;
safetyNote?: string;
steps: RemediationStep[];
}
export interface RemediationStep {
order: number;
description: string;
command: string;
commandType: string;
}
@Injectable({ providedIn: 'root' })
export class DoctorClient {
private readonly http = inject(HttpClient);
private readonly baseUrl = `${environment.apiUrl}/api/v1/doctor`;
listChecks(category?: string, plugin?: string): Observable<{ checks: CheckMetadata[]; total: number }> {
const params: Record<string, string> = {};
if (category) params['category'] = category;
if (plugin) params['plugin'] = plugin;
return this.http.get<{ checks: CheckMetadata[]; total: number }>(`${this.baseUrl}/checks`, { params });
}
listPlugins(): Observable<{ plugins: any[]; total: number }> {
return this.http.get<{ plugins: any[]; total: number }>(`${this.baseUrl}/plugins`);
}
startRun(request: RunDoctorRequest): Observable<{ runId: string }> {
return this.http.post<{ runId: string }>(`${this.baseUrl}/run`, request);
}
getRunResult(runId: string): Observable<DoctorReport> {
return this.http.get<DoctorReport>(`${this.baseUrl}/run/${runId}`);
}
streamRunProgress(runId: string): Observable<MessageEvent> {
return new Observable(observer => {
const eventSource = new EventSource(`${this.baseUrl}/run/${runId}/stream`);
eventSource.onmessage = event => observer.next(event);
eventSource.onerror = error => observer.error(error);
return () => eventSource.close();
});
}
listReports(limit = 20, offset = 0): Observable<{ reports: DoctorReport[]; total: number }> {
return this.http.get<{ reports: DoctorReport[]; total: number }>(
`${this.baseUrl}/reports`,
{ params: { limit: limit.toString(), offset: offset.toString() } }
);
}
}
```
---
### Task 4: State Store (Signal-based)
**Status:** TODO
```typescript
// services/doctor.store.ts
import { Injectable, signal, computed } from '@angular/core';
import { CheckResult, DoctorReport, DoctorSummary } from './doctor.client';
export type DoctorState = 'idle' | 'running' | 'completed' | 'error';
@Injectable({ providedIn: 'root' })
export class DoctorStore {
// State signals
readonly state = signal<DoctorState>('idle');
readonly currentRunId = signal<string | null>(null);
readonly report = signal<DoctorReport | null>(null);
readonly progress = signal<{ completed: number; total: number }>({ completed: 0, total: 0 });
readonly error = signal<string | null>(null);
// Filter signals
readonly categoryFilter = signal<string | null>(null);
readonly severityFilter = signal<string[]>([]);
readonly searchQuery = signal<string>('');
// Computed values
readonly summary = computed<DoctorSummary | null>(() => this.report()?.summary ?? null);
readonly filteredResults = computed<CheckResult[]>(() => {
const report = this.report();
if (!report) return [];
let results = report.results;
// Filter by category
const category = this.categoryFilter();
if (category) {
results = results.filter(r => r.category === category);
}
// Filter by severity
const severities = this.severityFilter();
if (severities.length > 0) {
results = results.filter(r => severities.includes(r.severity));
}
// Filter by search query
const query = this.searchQuery().toLowerCase();
if (query) {
results = results.filter(r =>
r.checkId.toLowerCase().includes(query) ||
r.diagnosis.toLowerCase().includes(query)
);
}
return results;
});
readonly failedResults = computed(() =>
this.report()?.results.filter(r => r.severity === 'fail') ?? []
);
readonly warningResults = computed(() =>
this.report()?.results.filter(r => r.severity === 'warn') ?? []
);
// Actions
startRun(runId: string, total: number) {
this.state.set('running');
this.currentRunId.set(runId);
this.progress.set({ completed: 0, total });
this.error.set(null);
}
updateProgress(completed: number, total: number) {
this.progress.set({ completed, total });
}
completeRun(report: DoctorReport) {
this.state.set('completed');
this.report.set(report);
}
setError(error: string) {
this.state.set('error');
this.error.set(error);
}
reset() {
this.state.set('idle');
this.currentRunId.set(null);
this.report.set(null);
this.progress.set({ completed: 0, total: 0 });
this.error.set(null);
}
}
```
---
### Task 5: Dashboard Component
**Status:** TODO
```typescript
// doctor-dashboard.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DoctorClient, RunDoctorRequest } from './services/doctor.client';
import { DoctorStore } from './services/doctor.store';
import { CheckListComponent } from './components/check-list/check-list.component';
import { SummaryStripComponent } from './components/summary-strip/summary-strip.component';
import { CheckResultComponent } from './components/check-result/check-result.component';
import { ExportDialogComponent } from './components/export-dialog/export-dialog.component';
@Component({
selector: 'app-doctor-dashboard',
standalone: true,
imports: [
CommonModule,
FormsModule,
CheckListComponent,
SummaryStripComponent,
CheckResultComponent,
ExportDialogComponent
],
templateUrl: './doctor-dashboard.component.html',
styleUrls: ['./doctor-dashboard.component.scss']
})
export class DoctorDashboardComponent implements OnInit {
private readonly client = inject(DoctorClient);
readonly store = inject(DoctorStore);
showExportDialog = false;
selectedResult: CheckResult | null = null;
ngOnInit() {
// Load previous report if available
}
runQuickCheck() {
this.runDoctor({ mode: 'quick' });
}
runFullCheck() {
this.runDoctor({ mode: 'full' });
}
private runDoctor(request: RunDoctorRequest) {
this.client.startRun(request).subscribe({
next: ({ runId }) => {
this.store.startRun(runId, 0);
this.pollForResults(runId);
},
error: err => this.store.setError(err.message)
});
}
private pollForResults(runId: string) {
// Use SSE for real-time updates
this.client.streamRunProgress(runId).subscribe({
next: event => {
const data = JSON.parse(event.data);
if (data.eventType === 'check-completed') {
this.store.updateProgress(data.completed, data.total);
} else if (data.eventType === 'run-completed') {
this.loadFinalResult(runId);
}
},
error: () => {
// Fallback to polling if SSE fails
this.pollWithInterval(runId);
}
});
}
private pollWithInterval(runId: string) {
const interval = setInterval(() => {
this.client.getRunResult(runId).subscribe(result => {
if (result.status === 'completed') {
clearInterval(interval);
this.store.completeRun(result);
}
});
}, 1000);
}
private loadFinalResult(runId: string) {
this.client.getRunResult(runId).subscribe({
next: result => this.store.completeRun(result),
error: err => this.store.setError(err.message)
});
}
openExportDialog() {
this.showExportDialog = true;
}
selectResult(result: CheckResult) {
this.selectedResult = result;
}
rerunCheck(checkId: string) {
this.runDoctor({ mode: 'normal', checkIds: [checkId] });
}
}
```
---
### Task 6: Dashboard Template
**Status:** TODO
```html
<!-- doctor-dashboard.component.html -->
<div class="doctor-dashboard">
<header class="dashboard-header">
<h1>Doctor Diagnostics</h1>
<div class="actions">
<button
class="btn btn-primary"
(click)="runQuickCheck()"
[disabled]="store.state() === 'running'">
Run Quick Check
</button>
<button
class="btn btn-secondary"
(click)="runFullCheck()"
[disabled]="store.state() === 'running'">
Run Full Check
</button>
<button
class="btn btn-outline"
(click)="openExportDialog()"
[disabled]="!store.report()">
Export Report
</button>
</div>
</header>
<!-- Filters -->
<div class="filters">
<select [(ngModel)]="store.categoryFilter" class="filter-select">
<option [ngValue]="null">All Categories</option>
<option value="core">Core</option>
<option value="database">Database</option>
<option value="servicegraph">Service Graph</option>
<option value="integration">Integration</option>
<option value="security">Security</option>
<option value="observability">Observability</option>
</select>
<div class="severity-filters">
<label>
<input type="checkbox" value="fail" (change)="toggleSeverity('fail')"> Failed
</label>
<label>
<input type="checkbox" value="warn" (change)="toggleSeverity('warn')"> Warnings
</label>
<label>
<input type="checkbox" value="pass" (change)="toggleSeverity('pass')"> Passed
</label>
</div>
<input
type="text"
placeholder="Search checks..."
class="search-input"
[(ngModel)]="store.searchQuery">
</div>
<!-- Progress (when running) -->
@if (store.state() === 'running') {
<div class="progress-bar">
<div
class="progress-fill"
[style.width.%]="(store.progress().completed / store.progress().total) * 100">
</div>
<span class="progress-text">
{{ store.progress().completed }} / {{ store.progress().total }} checks completed
</span>
</div>
}
<!-- Summary Strip -->
@if (store.summary(); as summary) {
<app-summary-strip [summary]="summary" [duration]="store.report()?.durationMs" />
}
<!-- Results List -->
<div class="results-container">
<div class="results-list">
@for (result of store.filteredResults(); track result.checkId) {
<app-check-result
[result]="result"
[expanded]="selectedResult?.checkId === result.checkId"
(click)="selectResult(result)"
(rerun)="rerunCheck(result.checkId)" />
}
@if (store.filteredResults().length === 0 && store.state() === 'completed') {
<div class="empty-state">
No checks match your filters
</div>
}
</div>
</div>
<!-- Export Dialog -->
@if (showExportDialog) {
<app-export-dialog
[report]="store.report()!"
(close)="showExportDialog = false" />
}
</div>
```
---
### Task 7: Check Result Component
**Status:** TODO
```typescript
// components/check-result/check-result.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CheckResult } from '../../services/doctor.client';
import { RemediationPanelComponent } from '../remediation-panel/remediation-panel.component';
import { EvidenceViewerComponent } from '../evidence-viewer/evidence-viewer.component';
@Component({
selector: 'app-check-result',
standalone: true,
imports: [CommonModule, RemediationPanelComponent, EvidenceViewerComponent],
templateUrl: './check-result.component.html',
styleUrls: ['./check-result.component.scss']
})
export class CheckResultComponent {
@Input({ required: true }) result!: CheckResult;
@Input() expanded = false;
@Output() rerun = new EventEmitter<void>();
get severityClass(): string {
return `severity-${this.result.severity}`;
}
get severityIcon(): string {
switch (this.result.severity) {
case 'pass': return 'check-circle';
case 'info': return 'info-circle';
case 'warn': return 'alert-triangle';
case 'fail': return 'x-circle';
case 'skip': return 'skip-forward';
default: return 'help-circle';
}
}
copyCommand(command: string) {
navigator.clipboard.writeText(command);
}
onRerun() {
this.rerun.emit();
}
}
```
---
### Task 8: Remediation Panel Component
**Status:** TODO
```typescript
// components/remediation-panel/remediation-panel.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Remediation } from '../../services/doctor.client';
@Component({
selector: 'app-remediation-panel',
standalone: true,
imports: [CommonModule],
template: `
<div class="remediation-panel">
@if (remediation.safetyNote) {
<div class="safety-note">
<span class="icon">!</span>
{{ remediation.safetyNote }}
</div>
}
@if (likelyCauses?.length) {
<div class="likely-causes">
<h4>Likely Causes</h4>
<ol>
@for (cause of likelyCauses; track $index) {
<li>{{ cause }}</li>
}
</ol>
</div>
}
<div class="fix-steps">
<h4>Fix Steps <button class="copy-all" (click)="copyAll()">Copy All</button></h4>
@for (step of remediation.steps; track step.order) {
<div class="step">
<div class="step-header">
<span class="step-number">{{ step.order }}.</span>
<span class="step-description">{{ step.description }}</span>
<button class="copy-btn" (click)="copy(step.command)">Copy</button>
</div>
<pre class="step-command"><code>{{ step.command }}</code></pre>
</div>
}
</div>
@if (verificationCommand) {
<div class="verification">
<h4>Verification</h4>
<pre class="verification-command">
<code>{{ verificationCommand }}</code>
<button class="copy-btn" (click)="copy(verificationCommand)">Copy</button>
</pre>
</div>
}
</div>
`,
styleUrls: ['./remediation-panel.component.scss']
})
export class RemediationPanelComponent {
@Input({ required: true }) remediation!: Remediation;
@Input() likelyCauses?: string[];
@Input() verificationCommand?: string;
copy(text: string) {
navigator.clipboard.writeText(text);
}
copyAll() {
const allCommands = this.remediation.steps
.map(s => `# ${s.order}. ${s.description}\n${s.command}`)
.join('\n\n');
navigator.clipboard.writeText(allCommands);
}
}
```
---
### Task 9: Test Suite
**Status:** TODO
```
src/app/features/doctor/__tests__/
├── doctor-dashboard.component.spec.ts
├── doctor.client.spec.ts
├── doctor.store.spec.ts
└── components/
├── check-result.component.spec.ts
└── remediation-panel.component.spec.ts
```
---
## Acceptance Criteria (Sprint)
- [ ] Dashboard accessible at /ops/doctor
- [ ] Quick and Full check buttons work
- [ ] Real-time progress via SSE
- [ ] Results display with severity icons
- [ ] Filtering by category, severity, search
- [ ] Expandable check results with evidence
- [ ] Remediation panel with copy buttons
- [ ] Export dialog for JSON/Markdown
- [ ] Responsive design for mobile
- [ ] Test coverage >= 80%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,620 @@
# SPRINT: Doctor Self-Service Features
> **Implementation ID:** 20260112
> **Sprint ID:** 001_009
> **Module:** LB (Library)
> **Status:** TODO
> **Created:** 12-Jan-2026
> **Depends On:** 001_006 (CLI)
---
## Overview
Implement self-service features that make the Doctor system truly useful for operators without requiring support escalation:
1. **Export & Share** - Generate shareable diagnostic bundles for support tickets
2. **Scheduled Checks** - Run doctor checks on a schedule with alerting
3. **Observability Plugin** - OTLP, logs, and metrics checks
4. **Auto-Remediation Suggestions** - Context-aware fix recommendations
---
## Working Directory
```
src/__Libraries/StellaOps.Doctor/
src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Observability/
src/Scheduler/
```
---
## Deliverables
### Task 1: Export Bundle Generator
**Status:** TODO
Generate comprehensive diagnostic bundles for support tickets.
```csharp
// Export/DiagnosticBundleGenerator.cs
public sealed class DiagnosticBundleGenerator
{
private readonly DoctorEngine _engine;
private readonly IConfiguration _configuration;
private readonly TimeProvider _timeProvider;
public DiagnosticBundleGenerator(
DoctorEngine engine,
IConfiguration configuration,
TimeProvider timeProvider)
{
_engine = engine;
_configuration = configuration;
_timeProvider = timeProvider;
}
public async Task<DiagnosticBundle> GenerateAsync(
DiagnosticBundleOptions options,
CancellationToken ct)
{
var report = await _engine.RunAsync(
new DoctorRunOptions { Mode = DoctorRunMode.Full },
cancellationToken: ct);
var bundle = new DiagnosticBundle
{
GeneratedAt = _timeProvider.GetUtcNow(),
Version = GetVersion(),
Environment = GetEnvironmentInfo(),
DoctorReport = report,
Configuration = options.IncludeConfig ? GetSanitizedConfig() : null,
Logs = options.IncludeLogs ? await CollectLogsAsync(options.LogDuration, ct) : null,
SystemInfo = await CollectSystemInfoAsync(ct)
};
return bundle;
}
public async Task<string> ExportToZipAsync(
DiagnosticBundle bundle,
string outputPath,
CancellationToken ct)
{
using var zipStream = File.Create(outputPath);
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
// Add doctor report
await AddJsonEntry(archive, "doctor-report.json", bundle.DoctorReport, ct);
// Add markdown summary
var markdownFormatter = new MarkdownReportFormatter();
var markdown = markdownFormatter.FormatReport(bundle.DoctorReport, new ReportFormatOptions
{
Verbose = true,
IncludeRemediation = true
});
await AddTextEntry(archive, "doctor-report.md", markdown, ct);
// Add environment info
await AddJsonEntry(archive, "environment.json", bundle.Environment, ct);
// Add system info
await AddJsonEntry(archive, "system-info.json", bundle.SystemInfo, ct);
// Add sanitized config if included
if (bundle.Configuration is not null)
{
await AddJsonEntry(archive, "config-sanitized.json", bundle.Configuration, ct);
}
// Add logs if included
if (bundle.Logs is not null)
{
foreach (var (name, content) in bundle.Logs)
{
await AddTextEntry(archive, $"logs/{name}", content, ct);
}
}
// Add README
await AddTextEntry(archive, "README.md", GenerateReadme(bundle), ct);
return outputPath;
}
private EnvironmentInfo GetEnvironmentInfo() => new()
{
Hostname = Environment.MachineName,
Platform = RuntimeInformation.OSDescription,
DotNetVersion = Environment.Version.ToString(),
ProcessId = Environment.ProcessId,
WorkingDirectory = Environment.CurrentDirectory,
StartTime = Process.GetCurrentProcess().StartTime.ToUniversalTime()
};
private async Task<SystemInfo> CollectSystemInfoAsync(CancellationToken ct)
{
var gcInfo = GC.GetGCMemoryInfo();
var process = Process.GetCurrentProcess();
return new SystemInfo
{
TotalMemoryBytes = gcInfo.TotalAvailableMemoryBytes,
ProcessMemoryBytes = process.WorkingSet64,
ProcessorCount = Environment.ProcessorCount,
Uptime = _timeProvider.GetUtcNow() - process.StartTime.ToUniversalTime()
};
}
private SanitizedConfiguration GetSanitizedConfig()
{
var sanitizer = new ConfigurationSanitizer();
return sanitizer.Sanitize(_configuration);
}
private async Task<Dictionary<string, string>> CollectLogsAsync(
TimeSpan duration,
CancellationToken ct)
{
var logs = new Dictionary<string, string>();
var logPaths = new[]
{
"/var/log/stellaops/gateway.log",
"/var/log/stellaops/scanner.log",
"/var/log/stellaops/orchestrator.log"
};
foreach (var path in logPaths)
{
if (File.Exists(path))
{
var content = await ReadRecentLinesAsync(path, 1000, ct);
logs[Path.GetFileName(path)] = content;
}
}
return logs;
}
private static string GenerateReadme(DiagnosticBundle bundle) => $"""
# Stella Ops Diagnostic Bundle
Generated: {bundle.GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC
Version: {bundle.Version}
Hostname: {bundle.Environment.Hostname}
## Contents
- `doctor-report.json` - Full diagnostic check results
- `doctor-report.md` - Human-readable report
- `environment.json` - Environment information
- `system-info.json` - System resource information
- `config-sanitized.json` - Sanitized configuration (if included)
- `logs/` - Recent log files (if included)
## Summary
- Passed: {bundle.DoctorReport.Summary.Passed}
- Warnings: {bundle.DoctorReport.Summary.Warnings}
- Failed: {bundle.DoctorReport.Summary.Failed}
## How to Use
Share this bundle with Stella Ops support by:
1. Creating a support ticket at https://support.stellaops.org
2. Attaching this ZIP file
3. Including any additional context about the issue
**Note:** This bundle has been sanitized to remove sensitive data.
Review contents before sharing externally.
""";
}
public sealed record DiagnosticBundle
{
public required DateTimeOffset GeneratedAt { get; init; }
public required string Version { get; init; }
public required EnvironmentInfo Environment { get; init; }
public required DoctorReport DoctorReport { get; init; }
public SanitizedConfiguration? Configuration { get; init; }
public Dictionary<string, string>? Logs { get; init; }
public required SystemInfo SystemInfo { get; init; }
}
public sealed record DiagnosticBundleOptions
{
public bool IncludeConfig { get; init; } = true;
public bool IncludeLogs { get; init; } = true;
public TimeSpan LogDuration { get; init; } = TimeSpan.FromHours(1);
}
```
---
### Task 2: CLI Export Command
**Status:** TODO
Add export subcommand to doctor:
```bash
# Generate diagnostic bundle
stella doctor export --output diagnostic-bundle.zip
# Include logs from last 4 hours
stella doctor export --output bundle.zip --include-logs --log-duration 4h
# Exclude configuration
stella doctor export --output bundle.zip --no-config
```
```csharp
// In DoctorCommandGroup.cs
var exportCommand = new Command("export", "Generate diagnostic bundle for support")
{
outputOption,
includeLogsOption,
logDurationOption,
noConfigOption
};
exportCommand.SetHandler(DoctorCommandHandlers.ExportAsync);
command.AddCommand(exportCommand);
```
---
### Task 3: Scheduled Doctor Checks
**Status:** TODO
Integrate doctor runs with the Scheduler service.
```csharp
// Scheduled/DoctorScheduleTask.cs
public sealed class DoctorScheduleTask : IScheduledTask
{
public string TaskType => "doctor-check";
public string DisplayName => "Scheduled Doctor Check";
private readonly DoctorEngine _engine;
private readonly INotificationService _notifications;
private readonly IReportStorageService _storage;
public DoctorScheduleTask(
DoctorEngine engine,
INotificationService notifications,
IReportStorageService storage)
{
_engine = engine;
_notifications = notifications;
_storage = storage;
}
public async Task ExecuteAsync(
ScheduledTaskContext context,
CancellationToken ct)
{
var options = context.GetOptions<DoctorScheduleOptions>();
var report = await _engine.RunAsync(
new DoctorRunOptions
{
Mode = options.Mode,
Categories = options.Categories?.ToImmutableArray()
},
cancellationToken: ct);
// Store report
await _storage.StoreReportAsync(report, ct);
// Send notifications based on severity
if (report.OverallSeverity >= DoctorSeverity.Warn)
{
await NotifyAsync(report, options, ct);
}
}
private async Task NotifyAsync(
DoctorReport report,
DoctorScheduleOptions options,
CancellationToken ct)
{
var notification = new DoctorAlertNotification
{
Severity = report.OverallSeverity,
Summary = $"Doctor found {report.Summary.Failed} failures, {report.Summary.Warnings} warnings",
ReportId = report.RunId,
FailedChecks = report.Results
.Where(r => r.Severity == DoctorSeverity.Fail)
.Select(r => r.CheckId)
.ToList()
};
foreach (var channel in options.NotificationChannels)
{
await _notifications.SendAsync(channel, notification, ct);
}
}
}
public sealed record DoctorScheduleOptions
{
public DoctorRunMode Mode { get; init; } = DoctorRunMode.Quick;
public IReadOnlyList<string>? Categories { get; init; }
public IReadOnlyList<string> NotificationChannels { get; init; } = ["slack", "email"];
public DoctorSeverity NotifyOnSeverity { get; init; } = DoctorSeverity.Warn;
}
```
---
### Task 4: CLI Schedule Command
**Status:** TODO
```bash
# Schedule daily doctor check
stella doctor schedule create --name daily-check --cron "0 6 * * *" --mode quick
# Schedule weekly full check with notifications
stella doctor schedule create --name weekly-full \
--cron "0 2 * * 0" \
--mode full \
--notify-channel slack \
--notify-on warn,fail
# List scheduled checks
stella doctor schedule list
# Delete scheduled check
stella doctor schedule delete --name daily-check
```
---
### Task 5: Observability Plugin
**Status:** TODO
```
StellaOps.Doctor.Plugin.Observability/
├── ObservabilityDoctorPlugin.cs
├── Checks/
│ ├── OtlpEndpointCheck.cs
│ ├── LogDirectoryCheck.cs
│ ├── LogRotationCheck.cs
│ └── PrometheusScapeCheck.cs
└── StellaOps.Doctor.Plugin.Observability.csproj
```
**Check Catalog:**
| CheckId | Name | Severity | Description |
|---------|------|----------|-------------|
| `check.telemetry.otlp.endpoint` | OTLP Endpoint | Warn | OTLP collector reachable |
| `check.logs.directory.writable` | Logs Writable | Fail | Log directory writable |
| `check.logs.rotation.configured` | Log Rotation | Warn | Log rotation configured |
| `check.metrics.prometheus.scrape` | Prometheus Scrape | Warn | Prometheus can scrape metrics |
**OtlpEndpointCheck:**
```csharp
public sealed class OtlpEndpointCheck : IDoctorCheck
{
public string CheckId => "check.telemetry.otlp.endpoint";
public string Name => "OTLP Endpoint";
public string Description => "Verify OTLP collector endpoint is reachable";
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
public IReadOnlyList<string> Tags => ["observability", "telemetry"];
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
public bool CanRun(DoctorPluginContext context)
{
var endpoint = context.Configuration["Telemetry:OtlpEndpoint"];
return !string.IsNullOrEmpty(endpoint);
}
public async Task<DoctorCheckResult> RunAsync(
DoctorPluginContext context,
CancellationToken ct)
{
var endpoint = context.Configuration["Telemetry:OtlpEndpoint"]!;
try
{
var httpClient = context.Services.GetRequiredService<IHttpClientFactory>()
.CreateClient("DoctorHealthCheck");
// OTLP gRPC or HTTP endpoint health check
var response = await httpClient.GetAsync($"{endpoint}/v1/health", ct);
if (response.IsSuccessStatusCode)
{
return context.CreateResult(CheckId)
.Pass("OTLP collector is reachable")
.WithEvidence(eb => eb.Add("Endpoint", endpoint))
.Build();
}
return context.CreateResult(CheckId)
.Warn($"OTLP collector returned {response.StatusCode}")
.WithEvidence(eb => eb
.Add("Endpoint", endpoint)
.Add("StatusCode", ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)))
.WithCauses(
"OTLP collector not running",
"Network connectivity issue",
"Wrong endpoint configured")
.WithRemediation(rb => rb
.AddStep(1, "Check OTLP collector status",
"docker logs otel-collector --tail 50",
CommandType.Shell)
.AddStep(2, "Test endpoint connectivity",
$"curl -v {endpoint}/v1/health",
CommandType.Shell))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (Exception ex)
{
return context.CreateResult(CheckId)
.Warn($"Cannot reach OTLP collector: {ex.Message}")
.WithEvidence(eb => eb.Add("Error", ex.Message))
.Build();
}
}
}
```
---
### Task 6: Configuration Sanitizer
**Status:** TODO
Safely export configuration without secrets.
```csharp
// Export/ConfigurationSanitizer.cs
public sealed class ConfigurationSanitizer
{
private static readonly HashSet<string> SensitiveKeys = new(StringComparer.OrdinalIgnoreCase)
{
"password", "secret", "key", "token", "apikey", "api_key",
"connectionstring", "connection_string", "credentials"
};
public SanitizedConfiguration Sanitize(IConfiguration configuration)
{
var result = new Dictionary<string, object>();
foreach (var section in configuration.GetChildren())
{
result[section.Key] = SanitizeSection(section);
}
return new SanitizedConfiguration
{
Values = result,
SanitizedKeys = GetSanitizedKeysList(configuration)
};
}
private object SanitizeSection(IConfigurationSection section)
{
if (!section.GetChildren().Any())
{
// Leaf value
if (IsSensitiveKey(section.Key))
{
return "***REDACTED***";
}
return section.Value ?? "(null)";
}
// Section with children
var result = new Dictionary<string, object>();
foreach (var child in section.GetChildren())
{
result[child.Key] = SanitizeSection(child);
}
return result;
}
private static bool IsSensitiveKey(string key)
{
return SensitiveKeys.Any(s => key.Contains(s, StringComparison.OrdinalIgnoreCase));
}
private IReadOnlyList<string> GetSanitizedKeysList(IConfiguration configuration)
{
var keys = new List<string>();
CollectSanitizedKeys(configuration, "", keys);
return keys;
}
private void CollectSanitizedKeys(IConfiguration config, string prefix, List<string> keys)
{
foreach (var section in config.GetChildren())
{
var fullKey = string.IsNullOrEmpty(prefix) ? section.Key : $"{prefix}:{section.Key}";
if (IsSensitiveKey(section.Key))
{
keys.Add(fullKey);
}
CollectSanitizedKeys(section, fullKey, keys);
}
}
}
public sealed record SanitizedConfiguration
{
public required Dictionary<string, object> Values { get; init; }
public required IReadOnlyList<string> SanitizedKeys { get; init; }
}
```
---
### Task 7: Test Suite
**Status:** TODO
```
src/__Tests/__Libraries/StellaOps.Doctor.Tests/
├── Export/
│ ├── DiagnosticBundleGeneratorTests.cs
│ └── ConfigurationSanitizerTests.cs
└── Scheduled/
└── DoctorScheduleTaskTests.cs
src/Doctor/__Tests/
└── StellaOps.Doctor.Plugin.Observability.Tests/
└── Checks/
├── OtlpEndpointCheckTests.cs
└── LogDirectoryCheckTests.cs
```
---
## CLI Commands Summary
```bash
# Export diagnostic bundle
stella doctor export --output bundle.zip
# Schedule checks
stella doctor schedule create --name NAME --cron CRON --mode MODE
stella doctor schedule list
stella doctor schedule delete --name NAME
stella doctor schedule run --name NAME
# View scheduled check history
stella doctor schedule history --name NAME --last 10
```
---
## Acceptance Criteria (Sprint)
- [ ] Diagnostic bundle generation with sanitization
- [ ] Export command in CLI
- [ ] Scheduled doctor checks with notifications
- [ ] Observability plugin with 4 checks
- [ ] Configuration sanitizer removes all secrets
- [ ] ZIP bundle contains README
- [ ] Test coverage >= 85%
---
## Execution Log
| Date | Entry |
|------|-------|
| 12-Jan-2026 | Sprint created |
| | |

View File

@@ -0,0 +1,101 @@
# Sprint 20260112_003_BE - C# Audit Pending Apply
## Topic & Scope
- Convert approved pending APPLY findings into remediation work across modules.
- Prioritize security, maintainability, and quality hotlists, then close production test and reuse gaps.
- Execute the remaining TODO APPLY backlog from the audit report and update the archived trackers.
- Pending APPLY status at sprint start: 107 DONE (waived/applied/revalidated), 851 TODO.
- **Working directory:** .; evidence: APPLY closures, test additions, and updated audit status.
## Dependencies & Concurrency
- Depends on archived audit report and maint/tests tracker in `docs-archived/implplan/2025-12-29-csproj-audit/`.
- Parallel execution is safe by module ownership; coordinate shared library changes.
## Documentation Prerequisites
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- docs/code-of-conduct/TESTING_PRACTICES.md
- docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_report.md
- docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md
- Module dossiers for affected projects (docs/modules/<module>/architecture.md).
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001 | TODO | Approved 2026-01-12; Hotlist S3/M1/Q0 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj`; apply fixes, add tests, update audit tracker. |
| 2 | AUDIT-HOTLIST-SCANNER-CONTRACTS-0001 | TODO | Approved 2026-01-12; Hotlist S3/M0/Q0 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Contracts/StellaOps.Scanner.Contracts.csproj`; apply fixes, add tests, update audit tracker. |
| 3 | AUDIT-HOTLIST-CLI-0001 | TODO | Approved 2026-01-12; Hotlist S2/M5/Q3 | Guild - CLI | Remediate hotlist findings for `src/Cli/StellaOps.Cli/StellaOps.Cli.csproj`; apply fixes, add tests, update audit tracker. |
| 4 | AUDIT-HOTLIST-EXPORTCENTER-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M4/Q0 | Guild - ExportCenter | Remediate hotlist findings for `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/StellaOps.ExportCenter.WebService.csproj`; apply fixes, add tests, update audit tracker. |
| 5 | AUDIT-HOTLIST-POLICY-ENGINE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M3/Q2 | Guild - Policy | Remediate hotlist findings for `src/Policy/StellaOps.Policy.Engine/StellaOps.Policy.Engine.csproj`; apply fixes, add tests, update audit tracker. |
| 6 | AUDIT-HOTLIST-SCANNER-NATIVE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M3/Q1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj`; apply fixes, add tests, update audit tracker. |
| 7 | AUDIT-HOTLIST-SCANNER-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M2/Q2 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj`; apply fixes, add tests, update audit tracker. |
| 8 | AUDIT-HOTLIST-EXPORTCENTER-CORE-0001 | TODO | Approved 2026-01-12; Hotlist S2/M2/Q1 | Guild - ExportCenter | Remediate hotlist findings for `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/StellaOps.ExportCenter.Core.csproj`; apply fixes, add tests, update audit tracker. |
| 9 | AUDIT-HOTLIST-SIGNALS-0001 | TODO | Approved 2026-01-12; Hotlist S2/M2/Q1 | Guild - Signals | Remediate hotlist findings for `src/Signals/StellaOps.Signals/StellaOps.Signals.csproj`; apply fixes, add tests, update audit tracker. |
| 10 | AUDIT-HOTLIST-SCANNER-LANG-DENO-0001 | TODO | Approved 2026-01-12; Hotlist S2/M0/Q0 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj`; apply fixes, add tests, update audit tracker. |
| 11 | AUDIT-HOTLIST-VEXLENS-0001 | TODO | Approved 2026-01-12; Hotlist S1/M4/Q0 | Guild - VexLens | Remediate hotlist findings for `src/VexLens/StellaOps.VexLens/StellaOps.VexLens.csproj`; apply fixes, add tests, update audit tracker. |
| 12 | AUDIT-HOTLIST-CONCELIER-CORE-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q2 | Guild - Concelier | Remediate hotlist findings for `src/Concelier/__Libraries/StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj`; apply fixes, add tests, update audit tracker. |
| 13 | AUDIT-HOTLIST-SCANNER-REACHABILITY-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj`; apply fixes, add tests, update audit tracker. |
| 14 | AUDIT-HOTLIST-EVIDENCE-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q0 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.Evidence/StellaOps.Evidence.csproj`; apply fixes, add tests, update audit tracker. |
| 15 | AUDIT-HOTLIST-ZASTAVA-OBSERVER-0001 | TODO | Approved 2026-01-12; Hotlist S1/M3/Q0 | Guild - Zastava | Remediate hotlist findings for `src/Zastava/StellaOps.Zastava.Observer/StellaOps.Zastava.Observer.csproj`; apply fixes, add tests, update audit tracker. |
| 16 | AUDIT-HOTLIST-TESTKIT-0001 | TODO | Approved 2026-01-12; Hotlist S0/M4/Q1 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj`; apply fixes, add tests, update audit tracker. |
| 17 | AUDIT-HOTLIST-EXCITITOR-WORKER-0001 | TODO | Approved 2026-01-12; Hotlist S0/M4/Q1 | Guild - Excititor | Remediate hotlist findings for `src/Excititor/StellaOps.Excititor.Worker/StellaOps.Excititor.Worker.csproj`; apply fixes, add tests, update audit tracker. |
| 18 | AUDIT-HOTLIST-SCANNER-WORKER-0001 | TODO | Approved 2026-01-12; Hotlist S0/M4/Q1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj`; apply fixes, add tests, update audit tracker. |
| 19 | AUDIT-HOTLIST-ROUTER-MICROSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist S0/M4/Q0 | Guild - Router | Remediate hotlist findings for `src/Router/__Libraries/StellaOps.Microservice/StellaOps.Microservice.csproj`; apply fixes, add tests, update audit tracker. |
| 20 | AUDIT-HOTLIST-CONCELIER-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist S0/M3/Q2 | Guild - Concelier | Remediate hotlist findings for `src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj`; apply fixes, add tests, update audit tracker. |
| 21 | AUDIT-HOTLIST-PROVCACHE-0001 | TODO | Approved 2026-01-12; Hotlist S0/M3/Q1 | Guild - Core | Remediate hotlist findings for `src/__Libraries/StellaOps.Provcache/StellaOps.Provcache.csproj`; apply fixes, add tests, update audit tracker. |
| 22 | AUDIT-HOTLIST-EXCITITOR-CORE-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S1/M2 | Guild - Excititor | Remediate hotlist findings for `src/Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj`; apply fixes, add tests, update audit tracker. |
| 23 | AUDIT-HOTLIST-SBOMSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S1/M2 | Guild - SbomService | Remediate hotlist findings for `src/SbomService/StellaOps.SbomService/StellaOps.SbomService.csproj`; apply fixes, add tests, update audit tracker. |
| 24 | AUDIT-HOTLIST-SCANNER-SBOMER-BUILDX-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S1/M2 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj`; apply fixes, add tests, update audit tracker. |
| 25 | AUDIT-HOTLIST-ATTESTOR-WEBSERVICE-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M2 | Guild - Attestor | Remediate hotlist findings for `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj`; apply fixes, add tests, update audit tracker. |
| 26 | AUDIT-HOTLIST-POLICY-TOOLS-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M1 | Guild - Policy | Remediate hotlist findings for `src/__Libraries/StellaOps.Policy.Tools/StellaOps.Policy.Tools.csproj`; apply fixes, add tests, update audit tracker. |
| 27 | AUDIT-HOTLIST-SCANNER-SOURCES-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M1 | Guild - Scanner | Remediate hotlist findings for `src/Scanner/__Libraries/StellaOps.Scanner.Sources/StellaOps.Scanner.Sources.csproj`; apply fixes, add tests, update audit tracker. |
| 28 | AUDIT-HOTLIST-BINARYINDEX-GOLDENSET-0001 | TODO | Approved 2026-01-12; Hotlist Q2/S0/M0 | Guild - BinaryIndex | Remediate hotlist findings for `src/BinaryIndex/__Libraries/StellaOps.BinaryIndex.GoldenSet/StellaOps.BinaryIndex.GoldenSet.csproj`; apply fixes, add tests, update audit tracker. |
| 29 | AUDIT-TESTGAP-DEVOPS-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - DevOps | Add tests and references for:<br>`devops/services/crypto/sim-crypto-service/SimCryptoService.csproj`<br>`devops/services/crypto/sim-crypto-smoke/SimCryptoSmoke.csproj`<br>`devops/services/cryptopro/linux-csp-service/CryptoProLinuxApi.csproj`<br>`devops/tools/nuget-prime/nuget-prime.csproj`<br>`devops/tools/nuget-prime/nuget-prime-v9.csproj`. |
| 30 | AUDIT-TESTGAP-DOCS-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Docs | Add test scaffolding or formal waivers for:<br>`docs/dev/sdks/plugin-templates/StellaOps.Templates.csproj`<br>`docs/dev/sdks/plugin-templates/stellaops-plugin-connector/StellaOps.Plugin.MyConnector.csproj`<br>`docs/dev/sdks/plugin-templates/stellaops-plugin-scheduler/StellaOps.Plugin.MyJob.csproj`. |
| 31 | AUDIT-TESTGAP-CRYPTO-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Cryptography | Add tests for:<br>`src/__Libraries/StellaOps.Cryptography.Plugin.Pkcs11Gost/StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj`<br>`src/__Libraries/StellaOps.Cryptography.Plugin.WineCsp/StellaOps.Cryptography.Plugin.WineCsp.csproj`<br>`src/__Libraries/StellaOps.Cryptography.Providers.OfflineVerification/StellaOps.Cryptography.Providers.OfflineVerification.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Eidas/StellaOps.Cryptography.Plugin.Eidas.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Fips/StellaOps.Cryptography.Plugin.Fips.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Gost/StellaOps.Cryptography.Plugin.Gost.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Hsm/StellaOps.Cryptography.Plugin.Hsm.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Sm/StellaOps.Cryptography.Plugin.Sm.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin/StellaOps.Cryptography.Plugin.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Profiles.Ecdsa/StellaOps.Cryptography.Profiles.Ecdsa.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Profiles.EdDsa/StellaOps.Cryptography.Profiles.EdDsa.csproj`<br>`src/Cryptography/StellaOps.Cryptography/StellaOps.Cryptography.csproj`. |
| 32 | AUDIT-TESTGAP-CORELIB-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Core | Add tests for:<br>`src/__Libraries/StellaOps.Infrastructure.EfCore/StellaOps.Infrastructure.EfCore.csproj`<br>`src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj`<br>`src/__Libraries/StellaOps.Orchestrator.Schemas/StellaOps.Orchestrator.Schemas.csproj`<br>`src/__Libraries/StellaOps.Policy.Tools/StellaOps.Policy.Tools.csproj`<br>`src/__Libraries/StellaOps.PolicyAuthoritySignals.Contracts/StellaOps.PolicyAuthoritySignals.Contracts.csproj`<br>`src/__Libraries/StellaOps.Provcache.Postgres/StellaOps.Provcache.Postgres.csproj`<br>`src/__Libraries/StellaOps.Provcache.Valkey/StellaOps.Provcache.Valkey.csproj`<br>`src/__Libraries/StellaOps.ReachGraph.Cache/StellaOps.ReachGraph.Cache.csproj`<br>`src/__Libraries/StellaOps.ReachGraph.Persistence/StellaOps.ReachGraph.Persistence.csproj`<br>`src/__Libraries/StellaOps.Signals.Contracts/StellaOps.Signals.Contracts.csproj`. |
| 33 | AUDIT-TESTGAP-ADVISORYAI-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - AdvisoryAI | Add tests for:<br>`src/AdvisoryAI/StellaOps.AdvisoryAI.Plugin.Unified/StellaOps.AdvisoryAI.Plugin.Unified.csproj`<br>`src/AdvisoryAI/StellaOps.AdvisoryAI.Scm.Plugin.Unified/StellaOps.AdvisoryAI.Scm.Plugin.Unified.csproj`<br>`src/AdvisoryAI/StellaOps.AdvisoryAI.Worker/StellaOps.AdvisoryAI.Worker.csproj`. |
| 34 | AUDIT-TESTGAP-AUTH-CONCELIER-ATTESTOR-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Module Leads | Add tests for:<br>`src/Attestor/StellaOps.Attestor.Types/Tools/StellaOps.Attestor.Types.Generator/StellaOps.Attestor.Types.Generator.csproj`<br>`src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Unified/StellaOps.Authority.Plugin.Unified.csproj`<br>`src/Concelier/__Libraries/StellaOps.Concelier.ProofService/StellaOps.Concelier.ProofService.csproj`<br>`src/Concelier/StellaOps.Concelier.Plugin.Unified/StellaOps.Concelier.Plugin.Unified.csproj`. |
| 35 | AUDIT-TESTGAP-SERVICES-CORE-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Platform Services | Add tests for:<br>`src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.csproj`<br>`src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Worker/StellaOps.EvidenceLocker.Worker.csproj`<br>`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj`<br>`src/Feedser/StellaOps.Feedser.BinaryAnalysis/StellaOps.Feedser.BinaryAnalysis.csproj`<br>`src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Infrastructure/StellaOps.IssuerDirectory.Infrastructure.csproj`<br>`src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/StellaOps.IssuerDirectory.WebService.csproj`<br>`src/Notify/__Libraries/StellaOps.Notify.Storage.InMemory/StellaOps.Notify.Storage.InMemory.csproj`<br>`src/OpsMemory/StellaOps.OpsMemory.WebService/StellaOps.OpsMemory.WebService.csproj`<br>`src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Worker/StellaOps.Orchestrator.Worker.csproj`<br>`src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Persistence.EfCore/StellaOps.PacksRegistry.Persistence.EfCore.csproj`<br>`src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Worker/StellaOps.PacksRegistry.Worker.csproj`. |
| 36 | AUDIT-TESTGAP-SERVICES-PLATFORM-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Platform Services | Add tests for:<br>`src/Policy/__Libraries/StellaOps.Policy.AuthSignals/StellaOps.Policy.AuthSignals.csproj`<br>`src/Policy/__Libraries/StellaOps.Policy.Explainability/StellaOps.Policy.Explainability.csproj`<br>`src/Policy/StellaOps.Policy.Registry/StellaOps.Policy.Registry.csproj`<br>`src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Worker/StellaOps.RiskEngine.Worker.csproj`<br>`src/Scheduler/StellaOps.Scheduler.Worker.Host/StellaOps.Scheduler.Worker.Host.csproj`<br>`src/Signals/StellaOps.Signals.Scheduler/StellaOps.Signals.Scheduler.csproj`<br>`src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/StellaOps.TaskRunner.Worker.csproj`<br>`src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/StellaOps.TimelineIndexer.WebService.csproj`<br>`src/Unknowns/__Libraries/StellaOps.Unknowns.Persistence.EfCore/StellaOps.Unknowns.Persistence.EfCore.csproj`<br>`src/VexHub/__Libraries/StellaOps.VexHub.Persistence/StellaOps.VexHub.Persistence.csproj`<br>`src/VexLens/StellaOps.VexLens.Persistence/StellaOps.VexLens.Persistence.csproj`<br>`src/VexLens/StellaOps.VexLens.WebService/StellaOps.VexLens.WebService.csproj`. |
| 37 | AUDIT-TESTGAP-INTEGRATIONS-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Integrations | Add tests for:<br>`src/Integrations/__Libraries/StellaOps.Integrations.Persistence/StellaOps.Integrations.Persistence.csproj`<br>`src/Integrations/__Plugins/StellaOps.Integrations.Plugin.Harbor/StellaOps.Integrations.Plugin.Harbor.csproj`<br>`src/Integrations/__Plugins/StellaOps.Integrations.Plugin.InMemory/StellaOps.Integrations.Plugin.InMemory.csproj`<br>`src/Plugin/StellaOps.Plugin.Sdk/StellaOps.Plugin.Sdk.csproj`. |
| 38 | AUDIT-TESTGAP-SCANNER-SBOM-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Scanner | Add tests for:<br>`src/SbomService/__Libraries/StellaOps.SbomService.Lineage/StellaOps.SbomService.Lineage.csproj`<br>`src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Native/StellaOps.Scanner.Analyzers.Native.csproj`<br>`src/Scanner/__Libraries/StellaOps.Scanner.ProofIntegration/StellaOps.Scanner.ProofIntegration.csproj`<br>`src/Scanner/StellaOps.Scanner.Analyzers.Plugin.Unified/StellaOps.Scanner.Analyzers.Plugin.Unified.csproj`. |
| 39 | AUDIT-TESTGAP-ROUTER-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Router | Add tests for:<br>`src/Router/__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj`<br>`src/Router/StellaOps.Router.Plugin.Unified/StellaOps.Router.Plugin.Unified.csproj`<br>`src/Router/examples/Examples.Billing.Microservice/Examples.Billing.Microservice.csproj`<br>`src/Router/examples/Examples.Gateway/Examples.Gateway.csproj`<br>`src/Router/examples/Examples.Inventory.Microservice/Examples.Inventory.Microservice.csproj`<br>`src/Router/examples/Examples.MultiTransport.Gateway/Examples.MultiTransport.Gateway.csproj`<br>`src/Router/examples/Examples.NotificationService/Examples.NotificationService.csproj`<br>`src/Router/examples/Examples.OrderService/Examples.OrderService.csproj`. |
| 40 | AUDIT-TESTGAP-SYMBOLS-0001 | TODO | Approved 2026-01-12; Production Test Gap Inventory | Guild - Symbols | Add tests for:<br>`src/Symbols/StellaOps.Symbols.Bundle/StellaOps.Symbols.Bundle.csproj`<br>`src/Symbols/StellaOps.Symbols.Client/StellaOps.Symbols.Client.csproj`<br>`src/Symbols/StellaOps.Symbols.Core/StellaOps.Symbols.Core.csproj`<br>`src/Symbols/StellaOps.Symbols.Infrastructure/StellaOps.Symbols.Infrastructure.csproj`<br>`src/Symbols/StellaOps.Symbols.Server/StellaOps.Symbols.Server.csproj`. |
| 41 | AUDIT-REUSE-DEVOPS-DOCS-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - DevOps/Docs | Resolve reuse gaps for:<br>`devops/services/crypto/sim-crypto-service/SimCryptoService.csproj`<br>`devops/services/cryptopro/linux-csp-service/CryptoProLinuxApi.csproj`<br>`devops/tools/nuget-prime/nuget-prime.csproj`<br>`devops/tools/nuget-prime/nuget-prime-v9.csproj`<br>`docs/dev/sdks/plugin-templates/StellaOps.Templates.csproj`<br>`docs/dev/sdks/plugin-templates/stellaops-plugin-connector/StellaOps.Plugin.MyConnector.csproj`<br>`docs/dev/sdks/plugin-templates/stellaops-plugin-scheduler/StellaOps.Plugin.MyJob.csproj`. |
| 42 | AUDIT-REUSE-CORELIBS-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - Core | Resolve reuse gaps for:<br>`src/__Libraries/StellaOps.Cryptography.Providers.OfflineVerification/StellaOps.Cryptography.Providers.OfflineVerification.csproj`<br>`src/__Libraries/StellaOps.Interop/StellaOps.Interop.csproj`<br>`src/__Libraries/StellaOps.Orchestrator.Schemas/StellaOps.Orchestrator.Schemas.csproj`<br>`src/__Libraries/StellaOps.PolicyAuthoritySignals.Contracts/StellaOps.PolicyAuthoritySignals.Contracts.csproj`<br>`src/__Libraries/StellaOps.Provcache.Postgres/StellaOps.Provcache.Postgres.csproj`<br>`src/__Libraries/StellaOps.Provcache.Valkey/StellaOps.Provcache.Valkey.csproj`<br>`src/__Libraries/StellaOps.Signals.Contracts/StellaOps.Signals.Contracts.csproj`. |
| 43 | AUDIT-REUSE-ADVISORY-AUTH-CONCELIER-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - Module Leads | Resolve reuse gaps for:<br>`src/AdvisoryAI/StellaOps.AdvisoryAI.Plugin.Unified/StellaOps.AdvisoryAI.Plugin.Unified.csproj`<br>`src/AdvisoryAI/StellaOps.AdvisoryAI.Scm.Plugin.Unified/StellaOps.AdvisoryAI.Scm.Plugin.Unified.csproj`<br>`src/AdvisoryAI/StellaOps.AdvisoryAI.Worker/StellaOps.AdvisoryAI.Worker.csproj`<br>`src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Unified/StellaOps.Authority.Plugin.Unified.csproj`<br>`src/Concelier/StellaOps.Concelier.Plugin.Unified/StellaOps.Concelier.Plugin.Unified.csproj`. |
| 44 | AUDIT-REUSE-CRYPTO-PROFILES-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - Cryptography | Resolve reuse gaps for:<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Eidas/StellaOps.Cryptography.Plugin.Eidas.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Fips/StellaOps.Cryptography.Plugin.Fips.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Gost/StellaOps.Cryptography.Plugin.Gost.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Hsm/StellaOps.Cryptography.Plugin.Hsm.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Plugin.Sm/StellaOps.Cryptography.Plugin.Sm.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Profiles.Ecdsa/StellaOps.Cryptography.Profiles.Ecdsa.csproj`<br>`src/Cryptography/StellaOps.Cryptography.Profiles.EdDsa/StellaOps.Cryptography.Profiles.EdDsa.csproj`. |
| 45 | AUDIT-REUSE-INTEGRATIONS-ROUTER-SCANNER-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - Integrations/Router/Scanner | Resolve reuse gaps for:<br>`src/Integrations/__Plugins/StellaOps.Integrations.Plugin.Harbor/StellaOps.Integrations.Plugin.Harbor.csproj`<br>`src/Integrations/__Plugins/StellaOps.Integrations.Plugin.InMemory/StellaOps.Integrations.Plugin.InMemory.csproj`<br>`src/Router/examples/Examples.Gateway/Examples.Gateway.csproj`<br>`src/Router/examples/Examples.MultiTransport.Gateway/Examples.MultiTransport.Gateway.csproj`<br>`src/Router/StellaOps.Router.Plugin.Unified/StellaOps.Router.Plugin.Unified.csproj`<br>`src/Scanner/__Libraries/StellaOps.Scanner.ProofIntegration/StellaOps.Scanner.ProofIntegration.csproj`<br>`src/Scanner/StellaOps.Scanner.Analyzers.Plugin.Unified/StellaOps.Scanner.Analyzers.Plugin.Unified.csproj`. |
| 46 | AUDIT-REUSE-SERVICES-CORE-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - Platform Services | Resolve reuse gaps for:<br>`src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Worker/StellaOps.EvidenceLocker.Worker.csproj`<br>`src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Worker/StellaOps.ExportCenter.Worker.csproj`<br>`src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/StellaOps.IssuerDirectory.WebService.csproj`<br>`src/Notify/__Libraries/StellaOps.Notify.Storage.InMemory/StellaOps.Notify.Storage.InMemory.csproj`<br>`src/OpsMemory/StellaOps.OpsMemory.WebService/StellaOps.OpsMemory.WebService.csproj`<br>`src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.Worker/StellaOps.Orchestrator.Worker.csproj`<br>`src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Persistence.EfCore/StellaOps.PacksRegistry.Persistence.EfCore.csproj`<br>`src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.Worker/StellaOps.PacksRegistry.Worker.csproj`. |
| 47 | AUDIT-REUSE-SERVICES-PLATFORM-0001 | TODO | Approved 2026-01-12; Production Reuse Gap Inventory | Guild - Platform Services | Resolve reuse gaps for:<br>`src/Policy/__Libraries/StellaOps.Policy.AuthSignals/StellaOps.Policy.AuthSignals.csproj`<br>`src/Policy/StellaOps.Policy.Registry/StellaOps.Policy.Registry.csproj`<br>`src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Worker/StellaOps.RiskEngine.Worker.csproj`<br>`src/Signals/StellaOps.Signals.Scheduler/StellaOps.Signals.Scheduler.csproj`<br>`src/Symbols/StellaOps.Symbols.Bundle/StellaOps.Symbols.Bundle.csproj`<br>`src/Symbols/StellaOps.Symbols.Server/StellaOps.Symbols.Server.csproj`<br>`src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.Worker/StellaOps.TaskRunner.Worker.csproj`<br>`src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/StellaOps.TimelineIndexer.WebService.csproj`<br>`src/VexLens/StellaOps.VexLens.WebService/StellaOps.VexLens.WebService.csproj`. |
| 48 | AUDIT-LONGTAIL-CORE-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Core | Batch remaining TODO APPLY items for shared libraries, analyzers, and test harnesses under `src/__Libraries`, `src/__Analyzers`, and `src/__Tests`; update audit tracker and evidence. |
| 49 | AUDIT-LONGTAIL-SCANNER-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Scanner | Batch remaining TODO APPLY items for Scanner projects (libraries, webservice, worker, analyzers, plugins); update audit tracker and evidence. |
| 50 | AUDIT-LONGTAIL-CONCELIER-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Concelier | Batch remaining TODO APPLY items for Concelier core, connectors, exporters, and web service; update audit tracker and evidence. |
| 51 | AUDIT-LONGTAIL-POLICY-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Policy | Batch remaining TODO APPLY items for Policy Engine and related libraries/tests; update audit tracker and evidence. |
| 52 | AUDIT-LONGTAIL-AUTH-ATTESTOR-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Authority/Attestor | Batch remaining TODO APPLY items for Authority, Attestor, Signer, and Registry projects; update audit tracker and evidence. |
| 53 | AUDIT-LONGTAIL-ROUTER-GRAPH-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Router/Graph | Batch remaining TODO APPLY items for Router, Gateway, Messaging, and Graph projects; update audit tracker and evidence. |
| 54 | AUDIT-LONGTAIL-NOTIFY-EXPORT-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Notify/ExportCenter | Batch remaining TODO APPLY items for Notify, ExportCenter, EvidenceLocker, Findings, and related services; update audit tracker and evidence. |
| 55 | AUDIT-LONGTAIL-ORCH-PLATFORM-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - Platform | Batch remaining TODO APPLY items for Orchestrator, PacksRegistry, Platform, Scheduler, Signals, TaskRunner, Timeline, and OpsMemory; update audit tracker and evidence. |
| 56 | AUDIT-LONGTAIL-DEVOPS-DOCS-0001 | TODO | Approved 2026-01-12; Apply Status Summary (TODO 851) | Guild - DevOps/Docs | Batch remaining TODO APPLY items for devops tools/services and docs templates; update audit tracker and evidence. |
| 57 | AUDIT-PENDING-TRACKER-0001 | TODO | After each remediation batch | Guild - PMO | Keep archived audit files and apply status summary in sync; record decisions/risks for each batch. |
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-01-12 | Archived SPRINT_20260112_002_BE_csproj_audit_apply_backlog.md to docs-archived/implplan/2026-01-12-csproj-audit-apply-backlog/. | Project Mgmt |
| 2026-01-12 | Expanded Delivery Tracker with per-project hotlist items and batched test/reuse gap remediation tasks. | Project Mgmt |
| 2026-01-12 | Set working directory to repo root to cover devops and docs items in test/reuse gaps. | Project Mgmt |
| 2026-01-12 | Sprint created to execute approved pending APPLY actions from the C# audit backlog. | Project Mgmt |
## Decisions & Risks
- APPROVED 2026-01-12: All pending APPLY actions are approved for execution under module review gates.
- Cross-module remediation touches many modules; mitigate with staged batches and explicit ownership.
- Cross-module doc link updates applied for archived audit files and the code-of-conduct relocation in docs/code-of-conduct/.
- Backlog size (851 TODO APPLY items); mitigate by prioritizing hotlists then long-tail batches.
- Devops and docs items are in scope; cross-directory changes must be logged per sprint guidance.
## Next Checkpoints
- TBD: Security hotlist remediation review.
- TBD: Test gap remediation checkpoint.

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ Authoritative sources for threat models, governance, compliance, and security op
## Policies & Governance
- [SECURITY_POLICY.md](../SECURITY_POLICY.md) - responsible disclosure, support windows.
- [GOVERNANCE.md](../GOVERNANCE.md) - project governance charter.
- [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) - community expectations.
- [CODE_OF_CONDUCT.md](../code-of-conduct/CODE_OF_CONDUCT.md) - community expectations.
- [SECURITY_HARDENING_GUIDE.md](../SECURITY_HARDENING_GUIDE.md) - deployment hardening steps.
- [policy-governance.md](./policy-governance.md) - policy governance specifics.
- [LEGAL_FAQ_QUOTA.md](../LEGAL_FAQ_QUOTA.md) - legal interpretation of quota.