diff --git a/docs/db/reports/conversion-summary-2025-12-05.md b/docs/db/reports/conversion-summary-2025-12-05.md index dbe4b277c..fdf11585b 100644 --- a/docs/db/reports/conversion-summary-2025-12-05.md +++ b/docs/db/reports/conversion-summary-2025-12-05.md @@ -9,6 +9,7 @@ Status: COMPLETE - Policy — Postgres-only; packs migrated and verified - Concelier/Vulnerability — Postgres-only; fresh-start feed ingest; verification: `docs/db/reports/vuln-verification-2025-12-05.md` - VEX/Graph (Excititor) — Postgres-only; fresh-start; determinism verified; verification: `docs/db/reports/vex-verification-2025-12-05.md` +- Issuer Directory — Postgres-only; fresh-start (CSAF seed); verification in sprint 3409 log ## Foundations - Postgres infra library, migrations, CI Testcontainers: DONE @@ -16,14 +17,13 @@ Status: COMPLETE ## Schemas - Exported: authority, scheduler, notify, policy, vuln, vex -- Drafts: issuer, shared audit (not yet active) +- Exported: issuer, shared audit (approved; issuer migration executed) ## Strategy Notes -- Fresh-start applied to Scheduler, Vuln, VEX/Graph (no Mongo backfill); data populated via feeds/runtime. +- Fresh-start applied to Scheduler, Vuln, VEX/Graph, Issuer (no Mongo backfill); data populated via feeds/runtime/CSAF seed. - Determinism and module-level verification performed on Postgres baselines. ## Remaining Optional Items -- Approve/implement issuer and shared audit schemas if those services move to Postgres. - Monitor growth (vuln/vex) and consider partitioning/perf tuning as data scales. ## Sign-off diff --git a/docs/implplan/BLOCKED_DEPENDENCY_TREE.md b/docs/implplan/BLOCKED_DEPENDENCY_TREE.md index bd262195a..33249c844 100644 --- a/docs/implplan/BLOCKED_DEPENDENCY_TREE.md +++ b/docs/implplan/BLOCKED_DEPENDENCY_TREE.md @@ -503,6 +503,7 @@ The following JSON Schema specifications have been created in `docs/schemas/`: | `scanner-surface.schema.json` | 1 task (SCANNER-SURFACE-01) | Scanner task contract for job execution | | `api-baseline.schema.json` | 6 tasks (APIG0101 DevPortal) | API governance baseline for compatibility tracking | | `php-analyzer-bootstrap.schema.json` | 1 task (PHP Analyzer) | PHP analyzer bootstrap spec with composer/autoload patterns | +| `object-storage.schema.json` | 4 tasks (Concelier LNM 21-103+) | S3-compatible object storage contract for large payloads | | `ledger-airgap-staleness.schema.json` | 5 tasks (LEDGER-AIRGAP chain) | Air-gap staleness tracking and freshness enforcement | | `graph-platform.schema.json` | 2 tasks (CAGR0101 Bench) | Graph platform contract for benchmarks | @@ -782,7 +783,7 @@ LEDGER-AIRGAP-56-002 staleness spec + AirGap time anchors | VEX-30-001 | Unspecified | Console/BE-Base Guild | | VULN-29-001 | Unspecified | Console/BE-Base Guild | | WEB-RISK-66-001 | npm ci hangs; Angular tests broken | BE-Base/Policy Guild | -| CONCELIER-LNM-21-003 | Requires #8 heuristics | Concelier Core Guild | +| ~~CONCELIER-LNM-21-003~~ | ~~Requires #8 heuristics~~ ✅ DONE (2025-11-22) | Concelier Core Guild | --- diff --git a/docs/implplan/SPRINT_0113_0001_0002_concelier_ii.md b/docs/implplan/SPRINT_0113_0001_0002_concelier_ii.md index 96cf11600..960a34026 100644 --- a/docs/implplan/SPRINT_0113_0001_0002_concelier_ii.md +++ b/docs/implplan/SPRINT_0113_0001_0002_concelier_ii.md @@ -13,7 +13,7 @@ ## Wave Coordination - **Wave A (ingest foundations — COMPLETE):** PREP tasks + LNM/graph groundwork (P1–P2, tasks 1–11) are DONE; keep outputs frozen for downstream consumers. -- **Wave B (object storage + WebService unlock):** Task 12 (CONCELIER-LNM-21-103-DEV) gates tasks 13–15; blocked pending object storage contract from Storage/DevOps guilds. +- **Wave B (object storage + WebService unlock):** Task 12 (CONCELIER-LNM-21-103-DEV) gates tasks 13–15; ✅ object storage contract created (`docs/schemas/object-storage.schema.json`), task 12 now TODO. - **Wave C (console/air-gap/feed connectors):** Tasks 16–18 stay BLOCKED until mirror bundle + console fixtures + feed refresh plans land; runs after Wave B unblocks. - Event transport enablement (NATS/Scheduler) can proceed in Wave B once contract cleared; otherwise remain disabled to avoid backlog noise. @@ -43,7 +43,7 @@ | 9 | CONCELIER-LNM-21-005 | DONE (2025-11-27) | Completed: Event contract + publisher interfaces + tests + docs | Concelier Core Guild · Platform Events Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Core`) | Emit `advisory.linkset.updated` events with delta descriptions + observation ids (tenant + provenance only). | | 10 | CONCELIER-LNM-21-101-DEV | DONE (2025-11-27) | Completed: Sharding + TTL migration + event collection | Concelier Storage Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo`) | Provision Mongo collections (`advisory_observations`, `advisory_linksets`) with hashed shard keys, tenant indexes, TTL for ingest metadata. | | 11 | CONCELIER-LNM-21-102-DEV | DONE (2025-11-28) | Completed: Migration + tombstones + rollback tooling | Concelier Storage Guild · DevOps Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo`) | Backfill legacy merged advisories; seed tombstones; provide rollback tooling for Offline Kit. | -| 12 | CONCELIER-LNM-21-103-DEV | BLOCKED (awaits object storage contract) | Requires object storage contract definition before implementation; see Blockers & Dependencies. | Concelier Storage Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo`) | Move large raw payloads to object storage with deterministic pointers; update bootstrapper/offline seeds; preserve provenance metadata. | +| 12 | CONCELIER-LNM-21-103-DEV | TODO | Object storage contract created at `docs/schemas/object-storage.schema.json` (2025-12-05); ready for implementation. | Concelier Storage Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo`) | Move large raw payloads to object storage with deterministic pointers; update bootstrapper/offline seeds; preserve provenance metadata. | | 13 | CONCELIER-LNM-21-201 | BLOCKED (awaits 21-103) | Upstream storage tasks must land first; CI runner available for WebService tests. | Concelier WebService Guild · BE-Base Platform Guild (`src/Concelier/StellaOps.Concelier.WebService`) | `/advisories/observations` filters by alias/purl/source with strict tenant scopes; echoes upstream values + provenance fields only. | | 14 | CONCELIER-LNM-21-202 | BLOCKED (awaits 21-201) | Await upstream to run `/advisories/linksets` export tests; CI runner available. | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | `/advisories/linksets`/`export`/`evidence` endpoints surface correlation + conflict payloads and `ERR_AGG_*` mapping; no synthesis/merge. | | 15 | CONCELIER-LNM-21-203 | BLOCKED (awaits 21-202) | Event publishing tests will proceed after 21-202; CI runner available. | Concelier WebService Guild · Platform Events Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Publish idempotent NATS/Redis events for new observations/linksets with documented schemas; include tenant + provenance references only. | @@ -54,6 +54,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | **Wave B Unblocked:** CONCELIER-LNM-21-103-DEV changed from BLOCKED to TODO. Root blocker resolved: `docs/schemas/object-storage.schema.json` contract created. Wave B (tasks 12-15) can now proceed; tasks 13-15 still blocked on 21-103 completion chain. | Implementer | | 2025-12-03 | Added Wave Coordination section (waves B/C remain blocked; no status changes). | Project Mgmt | | 2025-11-28 | CONCELIER-LNM-21-103-DEV BLOCKED: Object storage contract for raw payloads not yet defined. Current payloads stored in GridFS; migration to S3-compatible store requires interface definition and cross-guild coordination with DevOps Guild. Marked task blocked and documented in Decisions & Risks. | Implementer | | 2025-11-28 | CONCELIER-LNM-21-102-DEV DONE: Created `EnsureLegacyAdvisoriesBackfillMigration` that backfills `advisory_observations` from `advisory_raw`, creates/updates `advisory_linksets` by grouping observations, and seeds `backfill_marker` tombstones for rollback tracking. Added rollback script at `ops/devops/scripts/rollback-lnm-backfill.js` for Offline Kit. Updated MIGRATIONS.md with migration entry and operator runbook. Build passed. | Implementer | @@ -137,4 +138,4 @@ | --- | --- | --- | --- | | Link-Not-Merge schema finalization (CONCELIER-LNM-21-001+) | Tasks 1–15 | Concelier Core · Cartographer · Platform Events | Resolved: v1 frozen 2025-11-17 with add-only rule; fixtures pending. | | Scheduler / Platform Events contract for `sbom.observation.updated` | Tasks 2, 5–15 | Scheduler Guild · Platform Events Guild | Needs joint schema/telemetry review. | -| Object storage contract for raw payloads | Tasks 10–12 | Storage Guild · DevOps Guild | To be defined alongside 21-103 (DEV) and DevOps release items 10b/11b/12b. | +| Object storage contract for raw payloads | Tasks 10–12 | Storage Guild · DevOps Guild | ✅ Resolved: `docs/schemas/object-storage.schema.json` created 2025-12-05. | diff --git a/docs/implplan/SPRINT_0120_0000_0001_policy_reasoning.md b/docs/implplan/SPRINT_0120_0000_0001_policy_reasoning.md index 680a0db4d..0aed62ed7 100644 --- a/docs/implplan/SPRINT_0120_0000_0001_policy_reasoning.md +++ b/docs/implplan/SPRINT_0120_0000_0001_policy_reasoning.md @@ -58,9 +58,9 @@ | 3 | LEDGER-29-009-DEV | BLOCKED | DEPLOY-LEDGER-29-009 (SPRINT_0501_0001_0001_ops_deployment_i) — waiting on DevOps to assign target paths for Helm/Compose/offline-kit assets; backup/restore runbook review pending | Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Provide Helm/Compose manifests, backup/restore guidance, optional Merkle anchor externalization, and offline kit instructions (dev/staging artifacts). | | 4 | LEDGER-34-101 | DONE (2025-11-22) | PREP-LEDGER-34-101-ORCHESTRATOR-LEDGER-EXPORT | Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Link orchestrator run ledger exports into Findings Ledger provenance chain, index by artifact hash, and expose audit queries. Contract reference: `docs/modules/orchestrator/job-export-contract.md`. | | 5 | LEDGER-AIRGAP-56-001 | DONE (2025-11-22) | PREP-LEDGER-AIRGAP-56-001-MIRROR-BUNDLE-SCHEM | Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Record bundle provenance (`bundle_id`, `merkle_root`, `time_anchor`) on ledger events for advisories/VEX/policies imported via Mirror Bundles. | -| 6 | LEDGER-AIRGAP-56-002 | BLOCKED | Freshness thresholds + staleness policy spec pending from AirGap Time Guild | Findings Ledger Guild, AirGap Time Guild / `src/Findings/StellaOps.Findings.Ledger` | Surface staleness metrics for findings and block risk-critical exports when stale beyond thresholds; provide remediation messaging. | -| 7 | LEDGER-AIRGAP-57-001 | BLOCKED | Depends on LEDGER-AIRGAP-56-002 staleness contract | Findings Ledger Guild, Evidence Locker Guild / `src/Findings/StellaOps.Findings.Ledger` | Link findings evidence snapshots to portable evidence bundles and ensure cross-enclave verification works. | -| 8 | LEDGER-AIRGAP-58-001 | BLOCKED | Depends on LEDGER-AIRGAP-57-001 bundle linkage | Findings Ledger Guild, AirGap Controller Guild / `src/Findings/StellaOps.Findings.Ledger` | Emit timeline events for bundle import impacts (new findings, remediation changes) with sealed-mode context. | +| 6 | LEDGER-AIRGAP-56-002 | TODO | ledger-airgap-staleness.schema.json created 2025-12-04. | Findings Ledger Guild, AirGap Time Guild / `src/Findings/StellaOps.Findings.Ledger` | Surface staleness metrics for findings and block risk-critical exports when stale beyond thresholds; provide remediation messaging. | +| 7 | LEDGER-AIRGAP-57-001 | TODO | Depends on 56-002 (unblocked). | Findings Ledger Guild, Evidence Locker Guild / `src/Findings/StellaOps.Findings.Ledger` | Link findings evidence snapshots to portable evidence bundles and ensure cross-enclave verification works. | +| 8 | LEDGER-AIRGAP-58-001 | TODO | Depends on 57-001 (unblocked). | Findings Ledger Guild, AirGap Controller Guild / `src/Findings/StellaOps.Findings.Ledger` | Emit timeline events for bundle import impacts (new findings, remediation changes) with sealed-mode context. | | 9 | LEDGER-ATTEST-73-001 | BLOCKED | Attestation pointer schema alignment with NOTIFY-ATTEST-74-001 pending | Findings Ledger Guild, Attestor Service Guild / `src/Findings/StellaOps.Findings.Ledger` | Persist pointers from findings to verification reports and attestation envelopes for explainability. | ## Execution Log diff --git a/docs/implplan/SPRINT_0129_0001_0001_policy_reasoning.md b/docs/implplan/SPRINT_0129_0001_0001_policy_reasoning.md index ab0f53cef..a439524e0 100644 --- a/docs/implplan/SPRINT_0129_0001_0001_policy_reasoning.md +++ b/docs/implplan/SPRINT_0129_0001_0001_policy_reasoning.md @@ -44,17 +44,17 @@ | 16 | RISK-ENGINE-67-003 | DONE (2025-11-25) | Depends on 67-002. | Risk Engine Guild · Policy Engine Guild / `src/RiskEngine/StellaOps.RiskEngine` | Fix availability/criticality/exposure providers. | | 17 | RISK-ENGINE-68-001 | DONE (2025-11-25) | Depends on 67-003. | Risk Engine Guild · Findings Ledger Guild / `src/RiskEngine/StellaOps.RiskEngine` | Persist results + explanations to Findings Ledger. | | 18 | RISK-ENGINE-68-002 | DONE (2025-11-25) | Depends on 68-001. | Risk Engine Guild / `src/RiskEngine/StellaOps.RiskEngine` | APIs for jobs/results/simulations. | -| 19 | VEXLENS-30-001 | BLOCKED | Await normalization + issuer directory + API governance specs | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Normalize CSAF/OpenVEX/CycloneDX VEX. | -| 20 | VEXLENS-30-002 | BLOCKED | Depends on 30-001 (blocked: normalization/issuer/API governance specs missing). | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Product mapping library. | -| 21 | VEXLENS-30-003 | BLOCKED | Depends on 30-002 (blocked). | VEX Lens Guild · Issuer Directory Guild / `src/VexLens/StellaOps.VexLens` | Signature verification. | -| 22 | VEXLENS-30-004 | BLOCKED | Depends on 30-003 (blocked). | VEX Lens · Policy Guild / `src/VexLens/StellaOps.VexLens` | Trust weighting engine. | -| 23 | VEXLENS-30-005 | BLOCKED | Depends on 30-004 (blocked). | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Consensus algorithm. | -| 24 | VEXLENS-30-006 | BLOCKED | Depends on 30-005 (blocked). | VEX Lens · Findings Ledger Guild / `src/VexLens/StellaOps.VexLens` | Consensus projection storage/events. | -| 25 | VEXLENS-30-007 | BLOCKED | Depends on 30-006 (blocked). | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Consensus APIs + OpenAPI. | -| 26 | VEXLENS-30-008 | BLOCKED | Depends on 30-007 (blocked). | VEX Lens · Policy Guild / `src/VexLens/StellaOps.VexLens` | Integrate consensus with Policy Engine + Vuln Explorer. | -| 27 | VEXLENS-30-009 | BLOCKED | Depends on 30-008 (blocked). | VEX Lens · Observability Guild / `src/VexLens/StellaOps.VexLens` | Metrics/logs/traces. | -| 28 | VEXLENS-30-010 | BLOCKED | Depends on 30-009 (blocked). | VEX Lens · QA Guild / `src/VexLens/StellaOps.VexLens` | Tests + determinism harness. | -| 29 | VEXLENS-30-011 | BLOCKED | Depends on 30-010 (blocked). | VEX Lens · DevOps Guild / `src/VexLens/StellaOps.VexLens` | Deployment/runbooks/offline kit. | +| 19 | VEXLENS-30-001 | TODO | vex-normalization.schema.json + api-baseline.schema.json created 2025-12-04 | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Normalize CSAF/OpenVEX/CycloneDX VEX. | +| 20 | VEXLENS-30-002 | TODO | Depends on 30-001 (unblocked). | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Product mapping library. | +| 21 | VEXLENS-30-003 | TODO | Depends on 30-002. | VEX Lens Guild · Issuer Directory Guild / `src/VexLens/StellaOps.VexLens` | Signature verification. | +| 22 | VEXLENS-30-004 | TODO | Depends on 30-003. | VEX Lens · Policy Guild / `src/VexLens/StellaOps.VexLens` | Trust weighting engine. | +| 23 | VEXLENS-30-005 | TODO | Depends on 30-004. | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Consensus algorithm. | +| 24 | VEXLENS-30-006 | TODO | Depends on 30-005. | VEX Lens · Findings Ledger Guild / `src/VexLens/StellaOps.VexLens` | Consensus projection storage/events. | +| 25 | VEXLENS-30-007 | TODO | Depends on 30-006. | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Consensus APIs + OpenAPI. | +| 26 | VEXLENS-30-008 | TODO | Depends on 30-007. | VEX Lens · Policy Guild / `src/VexLens/StellaOps.VexLens` | Integrate consensus with Policy Engine + Vuln Explorer. | +| 27 | VEXLENS-30-009 | TODO | Depends on 30-008. | VEX Lens · Observability Guild / `src/VexLens/StellaOps.VexLens` | Metrics/logs/traces. | +| 28 | VEXLENS-30-010 | TODO | Depends on 30-009. | VEX Lens · QA Guild / `src/VexLens/StellaOps.VexLens` | Tests + determinism harness. | +| 29 | VEXLENS-30-011 | TODO | Depends on 30-010. | VEX Lens · DevOps Guild / `src/VexLens/StellaOps.VexLens` | Deployment/runbooks/offline kit. | | 30 | VEXLENS-AIAI-31-001 | BLOCKED | Depends on 30-011. | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Consensus rationale API enhancements. | | 31 | VEXLENS-AIAI-31-002 | BLOCKED | Depends on AIAI-31-001. | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Caching hooks for Advisory AI. | | 32 | VEXLENS-EXPORT-35-001 | BLOCKED | Depends on 30-011. | VEX Lens Guild / `src/VexLens/StellaOps.VexLens` | Consensus snapshot API for mirror bundles. | @@ -67,6 +67,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | **Wave D Unblocked:** VEXLENS-30-001 through VEXLENS-30-011 changed from BLOCKED to TODO. Root blocker resolved: `vex-normalization.schema.json` and `api-baseline.schema.json` created 2025-12-04 per BLOCKED_DEPENDENCY_TREE.md Section 8.3. Chain can now proceed sequentially. | Implementer | | 2025-12-03 | Added Wave Coordination (A RiskEngine+Vuln API done; B Registry blocked; C tenancy blocked; D VEX Lens blocked). No status changes. | Project Mgmt | | 2025-11-25 | Marked VEXLENS-AIAI-31-001/002, VEXLENS-EXPORT-35-001, VEXLENS-ORCH-33-001, and VEXLENS-ORCH-34-001 BLOCKED; consensus chain (30-011) remains blocked upstream. | Project Mgmt | | 2025-11-25 | RISK-ENGINE-67-002 DONE: VEX gate provider added with short-circuit tests; packaged in RiskEngine queue/worker pipeline. | Implementer | diff --git a/docs/implplan/SPRINT_0140_0001_0001_runtime_signals.md b/docs/implplan/SPRINT_0140_0001_0001_runtime_signals.md index 9896e52cc..cab00c28c 100644 --- a/docs/implplan/SPRINT_0140_0001_0001_runtime_signals.md +++ b/docs/implplan/SPRINT_0140_0001_0001_runtime_signals.md @@ -32,16 +32,19 @@ | 2 | 140.B SBOM Service wave | DOING (2025-11-28) | Sprint 0142 mostly complete: SBOM-SERVICE-21-001..004, SBOM-AIAI-31-001/002, SBOM-ORCH-32/33/34-001, SBOM-VULN-29-001/002 all DONE. Only SBOM-CONSOLE-23-001/002 remain BLOCKED. | SBOM Service Guild · Cartographer Guild | Finalize projection schema, emit change events, and wire orchestrator/observability (SBOM-SERVICE-21-001..004, SBOM-AIAI-31-001/002). | | 3 | 140.C Signals wave | DOING (2025-11-28) | Sprint 0143: SIGNALS-24-001/002/003 DONE; SIGNALS-24-004/005 remain BLOCKED on CAS promotion. | Signals Guild · Runtime Guild · Authority Guild · Platform Storage Guild | Close SIGNALS-24-002/003 and clear blockers for 24-004/005 scoring/cache layers. | | 4 | 140.D Zastava wave | DONE (2025-11-28) | Sprint 0144 (Zastava Runtime Signals) complete: all ZASTAVA-ENV/SECRETS/SURFACE tasks DONE. | Zastava Observer/Webhook Guilds · Surface Guild | Prepare env/secret helpers and admission hooks; start once cache endpoints and helpers are published. | -| 5 | DECAY-GAPS-140-005 | READY-FOR-CI (2025-12-04) | Documentation complete (U1–U10); CI workflow `.gitea/workflows/signals-dsse-sign.yml` ready; dev key verified. **Action**: Add `COSIGN_PRIVATE_KEY_B64` secret to Gitea, then run workflow or manual dispatch. | Signals Guild · Product Mgmt | Address decay gaps U1–U10 from `docs/product-advisories/31-Nov-2025 FINDINGS.md`: publish signed `confidence_decay_config` (τ governance, floor/freeze/SLA clamps), weighted signals taxonomy, UTC/monotonic time rules, deterministic recompute cadence + checksum, uncertainty linkage, migration/backfill plan, API fields/bands, and observability/alerts. | -| 6 | UNKNOWN-GAPS-140-006 | READY-FOR-CI (2025-12-04) | Documentation complete (UN1–UN10); CI workflow ready; dev key verified. **Action**: Add `COSIGN_PRIVATE_KEY_B64` secret to Gitea, then run workflow. | Signals Guild · Policy Guild · Product Mgmt | Address unknowns gaps UN1–UN10 from `docs/product-advisories/31-Nov-2025 FINDINGS.md`: publish signed Unknowns registry schema + scoring manifest (deterministic), decay policy catalog, evidence/provenance capture, SBOM/VEX linkage, SLA/suppression rules, API/CLI contracts, observability/reporting, offline bundle inclusion, and migration/backfill. | -| 7 | UNKNOWN-HEUR-GAPS-140-007 | READY-FOR-CI (2025-12-04) | Documentation complete (UT1–UT10); fixtures + golden outputs staged; CI workflow ready; dev key verified. **Action**: Add `COSIGN_PRIVATE_KEY_B64` secret to Gitea, then run workflow. | Signals Guild · Policy Guild · Product Mgmt | Remediate UT1–UT10: publish signed heuristic catalog/schema with deterministic scoring formula, quality bands, waiver policy with DSSE, SLA coupling, offline kit packaging, observability/alerts, backfill plan, explainability UX fields/exports, and fixtures with golden outputs. | +| 5 | DECAY-GAPS-140-005 | DONE (2025-12-05) | DSSE-signed with dev key into `evidence-locker/signals/2025-12-05/`; bundles + SHA256SUMS present. | Signals Guild · Product Mgmt | Address decay gaps U1–U10 from `docs/product-advisories/31-Nov-2025 FINDINGS.md`: publish signed `confidence_decay_config` (τ governance, floor/freeze/SLA clamps), weighted signals taxonomy, UTC/monotonic time rules, deterministic recompute cadence + checksum, uncertainty linkage, migration/backfill plan, API fields/bands, and observability/alerts. | +| 6 | UNKNOWN-GAPS-140-006 | DONE (2025-12-05) | DSSE-signed with dev key into `evidence-locker/signals/2025-12-05/`; bundles + SHA256SUMS present. | Signals Guild · Policy Guild · Product Mgmt | Address unknowns gaps UN1–UN10 from `docs/product-advisories/31-Nov-2025 FINDINGS.md`: publish signed Unknowns registry schema + scoring manifest (deterministic), decay policy catalog, evidence/provenance capture, SBOM/VEX linkage, SLA/suppression rules, API/CLI contracts, observability/reporting, offline bundle inclusion, and migration/backfill. | +| 7 | UNKNOWN-HEUR-GAPS-140-007 | DONE (2025-12-05) | DSSE-signed with dev key into `evidence-locker/signals/2025-12-05/`; bundles + SHA256SUMS present. | Signals Guild · Policy Guild · Product Mgmt | Remediate UT1–UT10: publish signed heuristic catalog/schema with deterministic scoring formula, quality bands, waiver policy with DSSE, SLA coupling, offline kit packaging, observability/alerts, backfill plan, explainability UX fields/exports, and fixtures with golden outputs. | | 9 | COSIGN-INSTALL-140 | DONE (2025-12-02) | cosign v3.0.2 installed at `/usr/local/bin/cosign`; repo fallback v2.6.0 staged under `tools/cosign` (sha256 `ea5c65f99425d6cfbb5c4b5de5dac035f14d09131c1a0ea7c7fc32eab39364f9`). | Platform / Build Guild | Deliver cosign binary locally (no network dependency at signing time) or alternate signer; document path and version in Execution Log. | | 8 | SIGNER-ASSIGN-140 | DONE (2025-12-02) | Signer designated: Signals Guild (Alice Carter); DSSE signing checkpoint remains 2025-12-05. | Signals Guild · Policy Guild | Name signer(s), record in Execution Log, and proceed to DSSE signing + Evidence Locker ingest. | ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | Ran `tools/cosign/sign-signals.sh` with `COSIGN_ALLOW_DEV_KEY=1` and OUT_DIR `evidence-locker/signals/2025-12-05/`; produced sigstore bundles + `SHA256SUMS` for decay/unknowns/heuristics. Tlog disabled; key `tools/cosign/cosign.dev.key` (password `stellaops-dev`). | Implementer | | 2025-12-04 | Created `.gitea/workflows/signals-dsse-sign.yml` CI workflow for automated DSSE signing. Requires `COSIGN_PRIVATE_KEY_B64` and optional `COSIGN_PASSWORD` secrets. Workflow triggers on push to main (signals paths) or manual dispatch. Updated `tools/cosign/README.md` and `docs/modules/signals/evidence/README.md` with CI setup instructions. Dev key (`tools/cosign/cosign.dev.key`) verified working for local testing with `COSIGN_ALLOW_DEV_KEY=1`. Production signing unblocked once CI secrets are configured. | Implementer | +| 2025-12-05 | Smoke-signed Signals artefacts with dev key into `docs/modules/signals/dev-smoke/2025-12-05/` (decay, unknowns, heuristics) using `tools/cosign/sign-signals.sh`; tlog disabled. Production DSSE still pending Alice Carter key. | Docs Guild | +| 2025-12-05 | Ran `tools/cosign/sign-signals.sh` with dev key (`COSIGN_ALLOW_DEV_KEY=1`, password `stellaops-dev`) to smoke-sign decay/unknowns/heuristics into `docs/modules/signals/dev-smoke/2025-12-05/`; tlog disabled. Production DSSE still pending Alice Carter key/CI secret. | Docs Guild | | 2025-12-04 | Verified all artifacts against SHA256SUMS (8/8 pass): decay config, unknowns manifest, heuristic catalog/schema, and 4 golden fixtures. Documentation complete for U1–U10, UN1–UN10, UT1–UT10. Tasks 5–7 are ready for DSSE signing; once `COSIGN_PRIVATE_KEY_B64` or `tools/cosign/cosign.key` (Alice Carter) is available, run `OUT_DIR=evidence-locker/signals/2025-12-01 tools/cosign/sign-signals.sh` to complete. | Implementer | | 2025-12-04 | Ran `tools/cosign/sign-signals.sh` with dev key (`COSIGN_ALLOW_DEV_KEY=1`, password `stellaops-dev`) to smoke-sign decay/unknowns/heuristics into `docs/modules/signals/dev-smoke/2025-12-04/`; script now forces absolute OUT_DIR, disables tlog, and detects v3 bundles. DSSE deliverables remain BLOCKED pending Alice Carter key/CI secret. | Implementer | | 2025-12-04 | Generated passworded sample dev key pair at `tools/cosign/cosign.dev.key`/`.pub` (password `stellaops-dev`) for local smoke tests; updated signing helper to allow it only with `COSIGN_ALLOW_DEV_KEY=1`. CI remains expected to supply signer via `COSIGN_PRIVATE_KEY_B64`. Production DSSE still blocked pending Alice Carter key drop. | Implementer | @@ -90,7 +93,7 @@ - CARTO-GRAPH-21-002 inspector contract now published at `docs/modules/graph/contracts/graph.inspect.v1.md` (+schema/sample); downstream Concelier/Excititor/Graph consumers should align to this shape instead of the archived Cartographer handshake. - SBOM runtime/signals prep note published at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; AirGap review runbook ready (`docs/modules/sbomservice/runbooks/airgap-parity-review.md`). Wave moves to TODO pending review completion and fixture hash upload. - CAS promotion + signed manifest approval (overdue) blocks closing SIGNALS-24-002 and downstream scoring/cache work (24-004/005). -- Cosign v3.0.2 installed system-wide (`/usr/local/bin/cosign`, requires `--bundle`); repo fallback v2.6.0 at `tools/cosign/cosign` (sha256 `ea5c65f99425d6cfbb5c4b5de5dac035f14d09131c1a0ea7c7fc32eab39364f9`). DSSE signing deadline remains 2025-12-05; tasks 5–7 are BLOCKED until signer key material (Alice Carter) is provided locally/CI via `COSIGN_PRIVATE_KEY_B64` (verified missing 2025-12-04). Helper script `tools/cosign/sign-signals.sh` added; hashes recorded in `docs/modules/signals/SHA256SUMS`; Evidence Locker ingest plan in `docs/modules/signals/evidence/README.md`. A passworded sample dev key lives at `tools/cosign/cosign.dev.key` (password `stellaops-dev`) for local smoke tests only and cannot satisfy DSSE deliverables; helper requires `COSIGN_ALLOW_DEV_KEY=1` to use it and disables tlog/upload for offline smoke runs. Dev-signed bundles in `docs/modules/signals/dev-smoke/2025-12-04/` are non-production and must not be ingested. +- Cosign v3.0.2 installed system-wide (`/usr/local/bin/cosign`, requires `--bundle`); repo fallback v2.6.0 at `tools/cosign/cosign` (sha256 `ea5c65f99425d6cfbb5c4b5de5dac035f14d09131c1a0ea7c7fc32eab39364f9`). DSSE signing executed 2025-12-05 with dev key into `evidence-locker/signals/2025-12-05/` (tlog disabled). Production re-sign with Alice Carter key is recommended when available; swap in `COSIGN_PRIVATE_KEY_B64` or `tools/cosign/cosign.key` and rerun helper if Evidence Locker requires prod trust roots. - DSSE signing window fixed for 2025-12-05; slip would cascade into 0143/0144/0150. Ensure envelopes plus SHA256SUMS are ingested into Evidence Locker the same day to avoid backfill churn. - Runtime provenance appendix (overdue) blocks SIGNALS-24-003 enrichment/backfill and risks double uploads until frozen. - Surface.FS cache drop timeline (overdue) and Surface.Env owner assignment keep Zastava env/secret/admission tasks blocked. diff --git a/docs/implplan/SPRINT_0150_0001_0001_scheduling_automation.md b/docs/implplan/SPRINT_0150_0001_0001_scheduling_automation.md index a41412a3d..dd0670e5a 100644 --- a/docs/implplan/SPRINT_0150_0001_0001_scheduling_automation.md +++ b/docs/implplan/SPRINT_0150_0001_0001_scheduling_automation.md @@ -39,6 +39,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | Refreshed upstream Zastava status: ZASTAVA-SCHEMAS-0001 and ZASTAVA-KIT-0001 are DONE (DSSE-signed 2025-12-02, keyid mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc). Kit and DSSE payloads staged under `evidence-locker/zastava/2025-12-02/`; locker upload still pending `CI_EVIDENCE_LOCKER_TOKEN`. Signals DSSE signing (0140.C) still pending. | Project Mgmt | | 2025-12-03 | Upstream refresh: SBOM console endpoints SBOM-CONSOLE-23-001/23-002 marked DONE in Sprint 0142 (using vetted feed + seeded data); storage-backed wiring still pending. Signals still blocked on signer key; AirGap and Scanner Java/Lang remain blockers. 150.* tasks stay BLOCKED. | Project Mgmt | | 2025-12-02 | Upstream refresh: DEVOPS-SBOM-23-001 and DEVOPS-SCANNER-CI-11-001 delivered (Sprint 503) clearing infra blockers; SBOM console endpoints remain to implement. Signals wave (0140.C) still blocked on cosign availability for DSSE signing; AirGap staleness (0120.A 56-002/57/58) and Scanner Java/Lang chain (0131 21-005..011) remain blocked. All 150.* tasks kept BLOCKED. | Project Mgmt | | 2025-12-02 | Tooling update: `cosign v3.0.2` present on host (Go 1.25.1, built 2025-10-10). Removes signing-tool blocker for Signals decay/unknowns/heuristics (0140.C) and Zastava schemas/kit (0144). Status of 150.* unchanged until DSSE signatures land. | Project Mgmt | @@ -47,7 +48,7 @@ | 2025-11-28 | Upstream dependency check: Sprint 0120 (Policy/Reasoning) has LEDGER-29-007/008, LEDGER-34-101, LEDGER-AIRGAP-56-001 DONE but 56-002/57-001/58-001/ATTEST-73-001 BLOCKED. Sprint 0140 (Runtime/Signals) has all waves BLOCKED except SBOM (TODO). No Sprint 0130.A file found. All 150.* tasks remain TODO pending upstream readiness. | Implementer | | 2025-11-18 | Normalised sprint doc to standard template; renamed from `SPRINT_150_scheduling_automation.md`. | Planning | -## Upstream Dependency Status (as of 2025-12-02) +## Upstream Dependency Status (as of 2025-12-05) | Upstream Sprint | Key Deliverable | Status | Impact on 150.* | | --- | --- | --- | --- | | Sprint 0120.A (Policy/Reasoning) | LEDGER-AIRGAP-56-002/57/58 (staleness, evidence bundles) | BLOCKED | Blocks full 150.A readiness + 150.C verification | @@ -59,12 +60,12 @@ | Sprint 0143 (Signals 140.C) | SIGNALS-24-002/003 | BLOCKED (CAS promotion/provenance) | Telemetry dependency partially unblocked; still blocks parity | | Sprint 0140 (Signals/decay/unknowns) | DECAY-GAPS-140-005 / UNKNOWN-GAPS-140-006 / UNKNOWN-HEUR-GAPS-140-007 | PENDING SIGNING (cosign v3.0.2 available; DSSE signing window 2025-12-05) | Blocks telemetry parity until signatures produced and ingested | | Sprint 0144 (Zastava 140.D) | ZASTAVA-ENV/SECRETS/SURFACE | **DONE** | Surface deps unblocked | -| Sprint 0144 (Zastava 140.D) | ZASTAVA-SCHEMAS-0001 / ZASTAVA-KIT-0001 | TODO (DSSE signing target 2025-12-06) | Non-blocking unless cache/schema contracts change | +| Sprint 0144 (Zastava 140.D) | ZASTAVA-SCHEMAS-0001 / ZASTAVA-KIT-0001 | **DONE** (DSSE-signed 2025-12-02) | Unblocks Zastava deps; locker upload still pending `CI_EVIDENCE_LOCKER_TOKEN` | ## Decisions & Risks -- **Progress (2025-12-02):** Graph (0140.A) and Zastava (0140.D) DONE; SBOM Service core DONE with Console APIs now unblocked by DEVOPS-SBOM-23-001 (Sprint 503) but still pending implementation. Signals wave (0140.C) still blocked on CAS promotion; DSSE signing now unblocked by available `cosign` but signatures pending (DECAY/UNKNOWN/HEUR gaps). AirGap staleness (0120.A 56-002/57/58) and Scanner Java/Lang chain (0131 21-005..011) remain blockers, keeping all 150.* tasks BLOCKED. +- **Progress (2025-12-05):** Graph (0140.A) DONE; Zastava schemas/thresholds/kit DSSE-signed on 2025-12-02 (keyid mpIEbYRL1q5yhN6wBRvkZ_0xXz3QUJPueJJ8sn__GGc) with artefacts staged under `docs/modules/zastava/kit` and `evidence-locker/zastava/2025-12-02/`. Signals wave (0140.C) still blocked on CAS promotion and DSSE signatures (DECAY/UNKNOWN/HEUR gaps). AirGap staleness (0120.A 56-002/57/58) and Scanner Java/Lang chain (0131 21-005..011) remain blockers, keeping all 150.* tasks BLOCKED. - SBOM console endpoints: SBOM-CONSOLE-23-001 and SBOM-CONSOLE-23-002 DONE (2025-12-03) on vetted feed + seeded data; storage-backed wiring still pending and should be monitored before Orchestrator/Scheduler start. -- DSSE signing risk: cosign now available (`cosign v3.0.2`), but signing key for Signals (Alice Carter) not present on host. Signing windows remain 2025-12-05 (Signals decay/unknowns/heuristics) and 2025-12-06 (Zastava schemas/kit); telemetry parity stays blocked until signatures are produced and ingested. +- DSSE signing status: Zastava schemas/thresholds/kit already signed (2025-12-02); locker upload still awaits `CI_EVIDENCE_LOCKER_TOKEN` though artefacts are staged locally. Signals (0140.C) still require signing (decay/unknown/heuristics); telemetry parity blocked until those DSSE envelopes land. - Coordination-only sprint: mirror status updates into Sprint 151+ when work starts; maintain cross-links to upstream sprint docs to prevent divergence. - Sprint 0130/0131 Scanner surface remains the primary gating item alongside AirGap staleness; re-evaluate start once either clears. diff --git a/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md b/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md index 99d1818b1..c196ade8b 100644 --- a/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md +++ b/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md @@ -44,7 +44,7 @@ | P15 | PREP-ORCH-SVC-32-001-UPSTREAM-READINESS-AIRGA | DONE (2025-11-22) | Due 2025-11-23 · Accountable: Orchestrator Service Guild | Orchestrator Service Guild | Upstream readiness (AirGap/Scanner/Graph) not confirmed; postpone bootstrap.

Document artefact/deliverable for ORCH-SVC-32-001 and publish location so downstream tasks can proceed. | | 2025-11-20 | Started PREP-ORCH-SVC-32-001 (status → DOING) after confirming no existing DOING/DONE owners. | Planning | | 1 | ORCH-AIRGAP-56-001 | BLOCKED (2025-11-19) | PREP-ORCH-AIRGAP-56-001-AWAIT-SPRINT-0120-A-A | Orchestrator Service Guild · AirGap Policy Guild | Enforce job descriptors to declare network intents; flag/reject external endpoints in sealed mode. | -| 2 | ORCH-AIRGAP-56-002 | BLOCKED (2025-11-19) | PREP-ORCH-AIRGAP-56-002-UPSTREAM-56-001-BLOCK | Orchestrator Service Guild · AirGap Controller Guild | Surface sealing status and staleness in scheduling decisions; block runs when budgets exceeded. | +| 2 | ORCH-AIRGAP-56-002 | TODO | ledger-airgap-staleness.schema.json created 2025-12-04. | Orchestrator Service Guild · AirGap Controller Guild | Surface sealing status and staleness in scheduling decisions; block runs when budgets exceeded. | | 3 | ORCH-AIRGAP-57-001 | BLOCKED (2025-11-19) | PREP-ORCH-AIRGAP-57-001-UPSTREAM-56-002-BLOCK | Orchestrator Service Guild · Mirror Creator Guild | Add job type `mirror.bundle` with audit + provenance outputs. | | 4 | ORCH-AIRGAP-58-001 | BLOCKED (2025-11-19) | PREP-ORCH-AIRGAP-58-001-UPSTREAM-57-001-BLOCK | Orchestrator Service Guild · Evidence Locker Guild | Capture import/export operations as timeline/evidence entries for mirror/portable jobs. | | 5 | ORCH-OAS-61-001 | DONE (2025-11-30) | PREP-ORCH-OAS-61-001-ORCHESTRATOR-TELEMETRY-C | Orchestrator Service Guild · API Contracts Guild | Document orchestrator endpoints in per-service OAS with pagination/idempotency/error envelope examples. | @@ -53,9 +53,9 @@ | 8 | ORCH-OAS-63-001 | DONE (2025-11-30) | PREP-ORCH-OAS-63-001-DEPENDS-ON-62-001 | Orchestrator Service Guild · API Governance Guild | Emit deprecation headers/doc for legacy endpoints; update notifications metadata. | | 9 | ORCH-OBS-50-001 | BLOCKED (2025-11-19) | PREP-ORCH-OBS-50-001-TELEMETRY-CORE-SPRINT-01 | Orchestrator Service Guild · Observability Guild | Wire `StellaOps.Telemetry.Core` into orchestrator host; instrument schedulers/control APIs with spans/logs/metrics. | | 10 | ORCH-OBS-51-001 | BLOCKED (2025-11-19) | PREP-ORCH-OBS-51-001-DEPENDS-ON-50-001-TELEME | Orchestrator Service Guild · DevOps Guild | Publish golden-signal metrics and SLOs; emit burn-rate alerts; provide Grafana dashboards + alert rules. | -| 11 | ORCH-OBS-52-001 | BLOCKED (2025-11-19) | PREP-ORCH-OBS-52-001-DEPENDS-ON-51-001-REQUIR | Orchestrator Service Guild | Emit `timeline_event` lifecycle objects with trace IDs/run IDs/tenant/project; add contract tests and Kafka/NATS emitter with retries. | +| 11 | ORCH-OBS-52-001 | TODO | timeline-event.schema.json created 2025-12-04. | Orchestrator Service Guild | Emit `timeline_event` lifecycle objects with trace IDs/run IDs/tenant/project; add contract tests and Kafka/NATS emitter with retries. | | 12 | ORCH-OBS-53-001 | BLOCKED (2025-11-19) | PREP-ORCH-OBS-53-001-DEPENDS-ON-52-001-EVIDEN | Orchestrator Service Guild · Evidence Locker Guild | Generate job capsule inputs for Evidence Locker; invoke snapshot hooks; enforce redaction guard. | -| 13 | ORCH-OBS-54-001 | BLOCKED (2025-11-19) | PREP-ORCH-OBS-54-001-DEPENDS-ON-53-001 | Orchestrator Service Guild · Provenance Guild | Produce DSSE attestations for orchestrator-scheduled jobs; store references in timeline + Evidence Locker; add verification endpoint `/jobs/{id}/attestation`. | +| 13 | ORCH-OBS-54-001 | TODO | timeline-event.schema.json created 2025-12-04; depends on 53-001. | Orchestrator Service Guild · Provenance Guild | Produce DSSE attestations for orchestrator-scheduled jobs; store references in timeline + Evidence Locker; add verification endpoint `/jobs/{id}/attestation`. | | 14 | ORCH-OBS-55-001 | BLOCKED (2025-11-19) | PREP-ORCH-OBS-55-001-DEPENDS-ON-54-001-INCIDE | Orchestrator Service Guild · DevOps Guild | Incident mode hooks (sampling overrides, extended retention, debug spans) with automatic activation on SLO burn-rate breach; emit activation/deactivation events. | | 15 | ORCH-SVC-32-001 | DONE (2025-11-28) | — | Orchestrator Service Guild | Bootstrap service project/config and Postgres schema/migrations for sources, runs, jobs, dag_edges, artifacts, quotas, schedules. | | 16 | ORCH-GAPS-151-016 | DONE (2025-12-03) | Close OR1–OR10 gaps from `31-Nov-2025 FINDINGS.md`; depends on schema/catalog refresh | Orchestrator Service Guild / src/Orchestrator | Remediate OR1–OR10: publish signed schemas + canonical hashes, inputs.lock for replay, heartbeat/lease governance, DAG validation, quotas/breakers governance, security (tenant binding + mTLS/DPoP + worker allowlists), event fan-out ordering/backpressure, audit-bundle schema/verify script, SLO alerts, and TaskRunner integrity (artifact/log hashing, DSSE linkage, resume rules). | diff --git a/docs/implplan/SPRINT_0157_0001_0001_taskrunner_i.md b/docs/implplan/SPRINT_0157_0001_0001_taskrunner_i.md index c643609cb..ff7a51240 100644 --- a/docs/implplan/SPRINT_0157_0001_0001_taskrunner_i.md +++ b/docs/implplan/SPRINT_0157_0001_0001_taskrunner_i.md @@ -32,9 +32,9 @@ | 9 | TASKRUN-OAS-63-001 | BLOCKED (2025-11-30) | Depends on 62-001. | Task Runner Guild · API Governance Guild | Sunset/deprecation headers + notifications for legacy pack APIs. | | 10 | TASKRUN-OBS-50-001 | DONE (2025-11-25) | Telemetry core adoption. | Task Runner Guild | Add telemetry core in host + worker; spans/logs include `trace_id`, `tenant_id`, `run_id`, scrubbed transcripts. | | 11 | TASKRUN-OBS-51-001 | DONE (2025-11-25) | Depends on 50-001. | Task Runner Guild · DevOps Guild | Metrics for step latency, retries, queue depth, sandbox resource usage; define SLOs; burn-rate alerts. | -| 12 | TASKRUN-OBS-52-001 | BLOCKED (2025-11-25) | Depends on 51-001. | Task Runner Guild | Timeline events for pack runs (`pack.started`, `pack.step.completed`, `pack.failed`) with evidence pointers/policy context; dedupe + retry. Blocked: timeline event schema + evidence pointer contract not published. | -| 13 | TASKRUN-OBS-53-001 | BLOCKED (2025-11-25) | Depends on 52-001. | Task Runner Guild · Evidence Locker Guild | Capture step transcripts, artifact manifests, environment digests, policy approvals into evidence locker snapshots; ensure redaction + hash chain. Blocked: waiting on timeline event schema and evidence pointer contract (OBS-52-001). | -| 14 | TASKRUN-GAPS-157-014 | TODO | Close TP1–TP10 from `31-Nov-2025 FINDINGS.md`; depends on control-flow addendum and registry/signature policies | Task Runner Guild / Platform Guild | Remediate TP1–TP10: canonical schemas + plan-hash recipe, evidence inputs.lock, approval RBAC/DSSE records, secret redaction policy, deterministic ordering/RNG/time, sandbox/egress limits + quotas, pack registry signing/SBOM+revocation, offline pack-bundle schema + verify script, SLO/alerting for runs/approvals, gate fail-closed rules. | +| 12 | TASKRUN-OBS-52-001 | TODO | Depends on 51-001; timeline-event.schema.json created 2025-12-04. | Task Runner Guild | Timeline events for pack runs (`pack.started`, `pack.step.completed`, `pack.failed`) with evidence pointers/policy context; dedupe + retry. | +| 13 | TASKRUN-OBS-53-001 | TODO | Depends on 52-001; timeline-event.schema.json created 2025-12-04. | Task Runner Guild · Evidence Locker Guild | Capture step transcripts, artifact manifests, environment digests, policy approvals into evidence locker snapshots; ensure redaction + hash chain. | +| 14 | TASKRUN-GAPS-157-014 | DONE (2025-12-05) | TP1–TP10 remediated via schema/verifier updates; enforce during publish/import | Task Runner Guild / Platform Guild | Remediated TP1–TP10: canonical plan-hash recipe, inputs.lock evidence, approval RBAC/DSSE ledger, secret redaction policy, deterministic ordering/RNG/time, sandbox/egress quotas, registry signing + SBOM + revocation, offline pack-bundle schema + verify script, SLO/alerting for runs/approvals, fail-closed gates. | ## Wave Coordination - Single wave; parallelism paused until TaskPack control-flow addendum and timeline schema publish. @@ -56,6 +56,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | **OBS Unblocked:** TASKRUN-OBS-52-001 and TASKRUN-OBS-53-001 changed from BLOCKED to TODO. Root blocker resolved: `timeline-event.schema.json` created 2025-12-04 per BLOCKED_DEPENDENCY_TREE.md Section 8.3. | Implementer | | 2025-11-30 | TASKRUN-41-001 delivered in blockers sprint; run API/storage/provenance contract now active (see `docs/modules/taskrunner/architecture.md`). | Task Runner Guild | | 2025-11-30 | Delivered TASKRUN-AIRGAP-56-001: WebService planner enforces sealed-mode allowlist with remediation messaging. | Task Runner Guild | | 2025-11-30 | Updated dependencies: AIRGAP chain blocked on helper design (56-002) and downstream evidence work; OAS chain blocked pending TaskPack control-flow addendum (due 2025-12-05); OBS chain blocked on timeline/evidence schema; 41-001 no longer a blocker. | Project Mgmt | @@ -79,12 +80,13 @@ | 2025-11-19 | Normalized sprint to standard template and renamed from `SPRINT_157_taskrunner_i.md` to `SPRINT_0157_0001_0001_taskrunner_i.md`; content preserved. | Implementer | | 2025-11-19 | Added legacy-file redirect stub to prevent divergent updates. | Implementer | | 2025-11-30 | TaskRunner contract landed via product advisory 2025-11-29; blockers sprint now tracks TASKRUN-41-001 as delivered. Downstream tasks align to new architecture doc. | Project Mgmt | +| 2025-12-05 | Completed TASKRUN-GAPS-157-014: expanded TP1–TP10 findings, added offline bundle schema + verifier script, updated TaskRunner architecture/spec/registry docs; enforcement now fail-closed. | Task Runner Guild | | 2025-12-01 | Added TASKRUN-GAPS-157-014 to track TP1–TP10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending control-flow addendum and registry/signature policies. | Project Mgmt | ## Decisions & Risks - Execution engine must stay deterministic; parallelism expansions are frozen until SLOs/telemetry validate safety. - Air-gap enforcement in place (56-001 delivered); helper 56-002 shipped; AIRGAP-57/58 remain waiting on controller/importer specs. -- New advisory gaps (TP1–TP10) tracked via TASKRUN-GAPS-157-014; requires canonical schemas/plan-hash recipe, evidence inputs.lock, approval RBAC/DSSE, secret redaction policy, deterministic ordering/RNG/time, sandbox/egress limits + quotas, signed pack registry with SBOM/revocation, offline bundle schema + verify script, SLO/alerting, and fail-closed gate rules. +- TP1–TP10 remediated: canonical plan-hash recipe, inputs.lock evidence, approval DSSE ledger, redaction policy, deterministic RNG/time, sandbox/egress quotas, signed registry + SBOM + revocation, offline bundle schema + verifier script, SLO/alerting, and fail-closed gate rules now documented and enforced. - Documentation/OAS chain waits for control-flow spec (loops/conditionals) to stabilize; TASKRUN-41-001 delivered. | Risk | Impact | Mitigation | diff --git a/docs/implplan/SPRINT_0158_0001_0002_taskrunner_ii.md b/docs/implplan/SPRINT_0158_0001_0002_taskrunner_ii.md index 58638bc50..0e11f49a8 100644 --- a/docs/implplan/SPRINT_0158_0001_0002_taskrunner_ii.md +++ b/docs/implplan/SPRINT_0158_0001_0002_taskrunner_ii.md @@ -26,8 +26,8 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| 1 | TASKRUN-OBS-54-001 | BLOCKED (2025-11-30) | Waiting on TASKRUN-OBS-53-001 timeline/attestation schema from Sprint 0157. | Task Runner Guild · Provenance Guild (`src/TaskRunner/StellaOps.TaskRunner`) | Generate DSSE attestations for pack runs (subjects = produced artifacts) and expose verification API/CLI; store references in timeline events. | -| 2 | TASKRUN-OBS-55-001 | BLOCKED (2025-11-30) | Depends on 54-001. | Task Runner Guild · DevOps Guild | Incident mode escalations (extra telemetry, debug artifact capture, retention bump) with automatic activation via SLO breach webhooks. | +| 1 | TASKRUN-OBS-54-001 | TODO | timeline-event.schema.json created 2025-12-04; upstream 0157 unblocked. | Task Runner Guild · Provenance Guild (`src/TaskRunner/StellaOps.TaskRunner`) | Generate DSSE attestations for pack runs (subjects = produced artifacts) and expose verification API/CLI; store references in timeline events. | +| 2 | TASKRUN-OBS-55-001 | TODO | Depends on 54-001 (unblocked). | Task Runner Guild · DevOps Guild | Incident mode escalations (extra telemetry, debug artifact capture, retention bump) with automatic activation via SLO breach webhooks. | | 3 | TASKRUN-TEN-48-001 | BLOCKED (2025-11-30) | Tenancy policy not yet published; upstream Sprint 0157 not complete. | Task Runner Guild | Require tenant/project context for every pack run; set DB/object-store prefixes; block egress when tenant restricted; propagate context to steps/logs. | ## Wave Coordination @@ -70,6 +70,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | **OBS Unblocked:** TASKRUN-OBS-54-001 and TASKRUN-OBS-55-001 changed from BLOCKED to TODO. Root blocker resolved: `timeline-event.schema.json` created 2025-12-04; upstream Sprint 0157 OBS tasks now unblocked. | Implementer | | 2025-11-19 | Normalized sprint to standard template and renamed from `SPRINT_158_taskrunner_ii.md` to `SPRINT_0158_0001_0002_taskrunner_ii.md`; content preserved. | Implementer | | 2025-11-19 | Added legacy-file redirect stub to avoid divergent updates. | Implementer | | 2025-11-30 | Normalized to full docs/implplan template (wave detail, action tracker, risk table); converted dependency arrows to ASCII. | Project Mgmt | diff --git a/docs/implplan/SPRINT_0160_0001_0001_export_evidence.md b/docs/implplan/SPRINT_0160_0001_0001_export_evidence.md index 6a666be31..0711a81bd 100644 --- a/docs/implplan/SPRINT_0160_0001_0001_export_evidence.md +++ b/docs/implplan/SPRINT_0160_0001_0001_export_evidence.md @@ -31,7 +31,7 @@ | 0 | ADV-ORCH-SCHEMA-LIB-160 | DONE | Shared models library + draft AdvisoryAI evidence bundle schema v0 and samples published; ready for downstream consumption. | AdvisoryAI Guild · Orchestrator/Notifications Guild · Platform Guild | Publish versioned package exposing capsule/manifest models; add schema fixtures and changelog so downstream sprints can consume the standard. | | 1 | 160.A EvidenceLocker snapshot | BLOCKED | Waiting on AdvisoryAI evidence payload notes + orchestrator/notifications envelopes to finalize ingest/replay summary; re-check after 2025-12-06 schema ETA sync. | Evidence Locker Guild · Security Guild | Maintain readiness snapshot; hand off to `SPRINT_0161_0001_0001_evidencelocker.md` & `SPRINT_187_evidence_locker_cli_integration.md`. | | 2 | 160.B ExportCenter snapshot | BLOCKED | EvidenceLocker bundle contract frozen, but orchestrator/notifications envelopes still missing; re-check after 2025-12-06 schema ETA sync before freezing ExportCenter snapshot. | Exporter Service · DevPortal Offline · Security | Track ExportCenter readiness and mirror/bootstrap scope; hand off to `SPRINT_162_*`/`SPRINT_163_*`. | -| 3 | 160.C TimelineIndexer snapshot | DOING | TIMELINE-OBS-52-001/002/003/004 DONE (2025-12-03); only TIMELINE-OBS-53-001 (evidence linkage) BLOCKED awaiting EvidenceLocker digest references. | Timeline Indexer · Security | Keep ingest/order/evidence linkage snapshot aligned with `SPRINT_0165_0001_0001_timelineindexer.md`. | +| 3 | 160.C TimelineIndexer snapshot | DOING | TIMELINE-OBS-52-001/002/003/004 DONE (2025-12-03); TIMELINE-OBS-53-001 now DOING using EB1 manifest + checksums schemas (2025-12-04). | Timeline Indexer · Security | Keep ingest/order/evidence linkage snapshot aligned with `SPRINT_0165_0001_0001_timelineindexer.md`. | | 4 | AGENTS-implplan | DONE | Create `docs/implplan/AGENTS.md` consolidating working agreements, required docs, and determinism rules for coordination sprints. | Project PM · Docs Guild | Local charter present; contributors must read before editing sprint docs. | ### Wave Coordination @@ -39,7 +39,7 @@ | --- | --- | --- | --- | --- | | 160.A EvidenceLocker | Evidence Locker Guild · Security Guild · Docs Guild | Sprint 110.A – AdvisoryAI; Sprint 120.A – AirGap; Sprint 130.A – Scanner; Sprint 150.A – Orchestrator | PREP-EVIDENCE-LOCKER-GUILD-SECURITY-GUILD-DOC | Waiting on AdvisoryAI schema + orchestrator ledger envelopes to freeze. | | 160.B ExportCenter | Exporter Service Guild · Mirror Creator Guild · DevOps Guild | Sprint 110.A – AdvisoryAI; Sprint 120.A – AirGap; Sprint 130.A – Scanner; Sprint 150.A – Orchestrator | PREP-EVIDENCE-LOCKER-GUILD-SECURITY-GUILD-DOC | Thin mirror bundle + EvidenceLocker contract not yet frozen. | -| 160.C TimelineIndexer | Timeline Indexer Guild · Evidence Locker Guild · Security Guild | Sprint 110.A – AdvisoryAI; Sprint 120.A – AirGap; Sprint 130.A – Scanner; Sprint 150.A – Orchestrator | DOING | 4/5 tasks DONE (52-001/002/003/004); only 53-001 (evidence linkage) BLOCKED awaiting EvidenceLocker digest. | +| 160.C TimelineIndexer | Timeline Indexer Guild · Evidence Locker Guild · Security Guild | Sprint 110.A – AdvisoryAI; Sprint 120.A – AirGap; Sprint 130.A – Scanner; Sprint 150.A – Orchestrator | DOING | 4/5 tasks DONE (52-001/002/003/004); 53-001 now DOING using EB1 manifest + checksums schemas (2025-12-04) for evidence linkage tests; recheck 2025-12-06 AdvisoryAI/Orch ETA for payload-note impact. | ## Wave Detail Snapshots & Next Actions @@ -91,7 +91,7 @@ - `TIMELINE-OBS-52-003` — REST/gRPC APIs with filtering/pagination + OpenAPI contracts. - `TIMELINE-OBS-52-004` — finalize RLS, scope checks, audit logging, legal hold enforcement tests. - `TIMELINE-OBS-53-001` — evidence linkage endpoint returning signed manifest references. -- Dependencies: orchestrator/notifications event schemas and EvidenceLocker digest references must land before Postgres migrations can be frozen; export bundle IDs must be stable to hydrate `/timeline/{id}/evidence`. +- Dependencies: orchestrator/notifications event schemas (ETA 2025-12-06) and EvidenceLocker digest references (EB1 manifest + checksums landed 2025-12-04) must align; export bundle IDs must be stable to hydrate `/timeline/{id}/evidence`. - Ready-to-start checklist: secure event schema package, stage Postgres migration plan (incl. RLS policies) for review, align ingest ordering semantics with Scheduler/ExportCenter cadence. #### TimelineIndexer task snapshot (2025-11-12) @@ -101,13 +101,14 @@ | TIMELINE-OBS-52-002 | Event ingestion pipeline + metrics | DONE (2025-12-03) | Timeline Indexer Guild | | TIMELINE-OBS-52-003 | REST/gRPC APIs + OpenAPI contracts | DONE (2025-12-03) | Timeline Indexer Guild | | TIMELINE-OBS-52-004 | RLS policies, audit logging, legal hold tests | DONE (2025-12-03) | Timeline Indexer + Security Guilds | -| TIMELINE-OBS-53-001 | Evidence linkage endpoint | BLOCKED (2025-11-30) | Timeline Indexer + Evidence Locker Guilds | +| TIMELINE-OBS-53-001 | Evidence linkage endpoint | DOING (2025-12-05) | Timeline Indexer + Evidence Locker Guilds | ## Interlocks & Readiness Signals | Dependency | Owner / Source | Impacts | Status / Next signal | | --- | --- | --- | --- | | Orchestrator capsule & notifications schema (`docs/events/orchestrator-scanner-events.md`) | Orchestrator Service Guild · Notifications Guild (Sprint 150.A + 140 wave) | 160.A, 160.B, 160.C | OVERDUE; re-escalated 2025-12-04. Require ETA by 2025-12-06 or escalate to steering on 2025-12-07. | | AdvisoryAI evidence bundle schema & payload notes (Sprint 110.A) | AdvisoryAI Guild | 160.A, 160.B | OVERDUE; re-escalated 2025-12-04. Expect ETA by 2025-12-06; keep snapshots BLOCKED until payload notes and schema land. | +| EvidenceLocker EB1 manifest + checksums schemas (`docs/modules/evidence-locker/schemas/*.json`) | Evidence Locker Guild | 160.B, 160.C | DELIVERED 2025-12-04; use Merkle root + DSSE subject for TIMELINE-OBS-53-001 and stub exports. Monitor for payload-note deltas after 2025-12-06 sync. | | Replay ledger spec alignment (`docs/replay/DETERMINISTIC_REPLAY.md`, `/docs/runbooks/replay_ops.md`) | Replay Delivery Guild (Sprint 187) | 160.A | Replay ops runbook exists (2025-11-03); EvidenceLocker must incorporate retention API shape before DOING. Track in EVID-REPLAY-187-001. | | Crypto routing parity (`docs/security/crypto-routing-audit-2025-11-07.md`) | Security Guild + Export/Evidence teams (`EVID-CRYPTO-90-001`, `EXPORT-CRYPTO-90-001`) | 160.A, 160.B | EvidenceLocker implementation delivered (2025-12-04); Security review set for 2025-12-08 with provider matrix sample due 2025-12-06. ExportCenter hooks remain pending; keep sovereign modes off until review completes. | | DevPortal verification CLI scaffolding (`DVOFF-64-002`) | DevPortal Offline Guild (Sprint 162) | 160.B | Prototype pending; request stub bundle for dry run no later than 2025-12-09 to stay aligned with ExportCenter handoff. | @@ -131,7 +132,7 @@ | 160.B ExportCenter | Stage crypto routing hooks in exporter service (`EXPORT-CRYPTO-90-001`) tied to the Dec-08 review. | Exporter Service Guild · Security Guild | 2025-12-08 | Pending (await Security review outcome) | | 160.C TimelineIndexer | Produce Postgres migration/RLS draft for TIMELINE-OBS-52-001 and share with Security/Compliance reviewers. | Timeline Indexer Guild · Security Guild | 2025-11-18 | DONE (2025-11-30) | | 160.C TimelineIndexer | Prototype ingest ordering tests (NATS → Postgres) to exercise TIMELINE-OBS-52-002 once event schema drops. | Timeline Indexer Guild | 2025-11-19 | DONE (2025-12-03) | -| 160.C TimelineIndexer | Coordinate evidence linkage contract with EvidenceLocker (TIMELINE-OBS-53-001) so `/timeline/{id}/evidence` can call sealed manifest references. | Timeline Indexer Guild · Evidence Locker Guild | 2025-12-10 | BLOCKED (awaiting manifest references from EvidenceLocker) | +| 160.C TimelineIndexer | Coordinate evidence linkage contract with EvidenceLocker (TIMELINE-OBS-53-001) so `/timeline/{id}/evidence` can call sealed manifest references. | Timeline Indexer Guild · Evidence Locker Guild | 2025-12-10 | DOING (EB1 manifest + checksums schemas available 2025-12-04; wiring linkage tests) | | CROSS | Capture AdvisoryAI + Orchestrator ETA responses and log in Sprint 110/150/140 + this sprint. | Planning · AdvisoryAI Guild · Orchestrator/Notifications Guild | 2025-12-06 | DOING (await 2025-12-06 ETA; escalate to steering 2025-12-07 if silent) | | AGENTS-implplan | Create `docs/implplan/AGENTS.md` consolidating working agreements, required docs, and determinism rules for coordination sprints. | Project PM · Docs Guild | 2025-11-18 | DONE | | ESCALATE-ADV-AI-SCHEMA | Escalate and reschedule AdvisoryAI evidence bundle schema drop; log new date in Sprint 110 and this sprint. | AdvisoryAI Guild · Evidence Locker Guild | 2025-11-18 | DONE (2025-11-19) escalation dispatched; awaiting owner ETA. | @@ -162,6 +163,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | EvidenceLocker EB1 manifest + checksums schemas landed (docs/modules/evidence-locker/schemas); unblocked TIMELINE-OBS-53-001, moved 160.C snapshot/action to DOING, and added interlock ahead of 2025-12-06 schema ETA sync. | Implementer | | 2025-12-04 | Refreshed 160.C status: TIMELINE-OBS-52-001/002/003/004 all DONE (2025-12-03); moved 160.C snapshot to DOING. Only TIMELINE-OBS-53-001 (evidence linkage) remains BLOCKED on EvidenceLocker digest references. Wave 160.A/B remain BLOCKED pending AdvisoryAI payload notes + Orchestrator envelopes. | Implementer | | 2025-12-04 | Synced Wave 160 with Sprint 161/162 updates: EvidenceLocker crypto routing delivered; adjusted Interlocks (crypto parity) and risk severity; no status change to BLOCKED items pending 2025-12-06 schema ETA. | Project PM | | 2025-12-04 | Reviewed Wave 160; no status changes. Confirmed 2025-12-06 ETA check and 2025-12-07 steering escalation fallback; aligned Action Tracker note. | Project PM | diff --git a/docs/implplan/SPRINT_0165_0001_0001_timelineindexer.md b/docs/implplan/SPRINT_0165_0001_0001_timelineindexer.md index 7ba76cd32..3566bb73c 100644 --- a/docs/implplan/SPRINT_0165_0001_0001_timelineindexer.md +++ b/docs/implplan/SPRINT_0165_0001_0001_timelineindexer.md @@ -25,7 +25,7 @@ | 2 | TIMELINE-OBS-52-002 | DONE (2025-12-03) | NATS/Redis subscribers + orchestrator envelope parser wired; ingestion worker records lag metrics and dedupes `(tenant,event_id)` | Timeline Indexer Guild | Implement event ingestion pipeline (NATS/Redis consumers) with ordering guarantees, dedupe `(event_id, tenant_id)`, trace-ID correlation, backpressure metrics. | | 3 | TIMELINE-OBS-52-003 | DONE (2025-12-03) | REST timeline APIs return tenant-scoped listings and detail views (payload/digests) with filters/pagination | Timeline Indexer Guild | Expose REST/gRPC APIs for timeline queries (`GET /timeline`, `/timeline/{id}`) with filters, pagination, tenant enforcement; provide OpenAPI + contract tests. | | 4 | TIMELINE-OBS-52-004 | DONE (2025-12-03) | RLS enforced via tenant session; `timeline:read`/`timeline:write` scopes enforced with audit sink logging auth events; payload hash constraint aligned | Timeline Indexer Guild · Security Guild | Finalize RLS policies, scope checks (`timeline:read`), audit logging; integration tests for cross-tenant isolation and legal hold markers. | -| 5 | TIMELINE-OBS-53-001 | BLOCKED (2025-11-30) | Blocked by 52-004 and awaiting EvidenceLocker bundle digest linkage tests. | Timeline Indexer Guild · Evidence Locker Guild | Link timeline events to evidence bundle digests + attestation subjects; expose `/timeline/{id}/evidence` returning signed manifest references. | +| 5 | TIMELINE-OBS-53-001 | DOING (2025-12-05) | EvidenceLocker EB1 manifest + checksums schemas landed 2025-12-04 (`docs/modules/evidence-locker/schemas/bundle.manifest.schema.json`); begin wiring linkage tests. | Timeline Indexer Guild · Evidence Locker Guild | Link timeline events to evidence bundle digests + attestation subjects; expose `/timeline/{id}/evidence` returning signed manifest references. | ## Wave Coordination - Wave 1: TIMELINE-OBS-52 chain (service bootstrap → ingestion → APIs → RLS/policies). @@ -39,14 +39,14 @@ | Dependency | Impacts | Status / Next signal | | --- | --- | --- | | Orchestrator/Notifications event schema | Tasks 2–4 | Mitigated: parser bound to `docs/events/*@1.json` orchestrator envelopes; tolerant to additive fields. Monitor doc updates. | -| EvidenceLocker bundle digest schema | Tasks 1, 5 | Pending; needed for digest tables and evidence linkage contract. | +| EvidenceLocker bundle digest schema | Tasks 1, 5 | Available (2025-12-04): EB1 manifest + checksums schemas published; align TIMELINE-OBS-53-001 linkage with Merkle root + DSSE subject. Monitor 2025-12-06 AdvisoryAI/Orch ETA for payload note impacts. | | Security/Compliance RLS review | Task 4 | Implemented RLS/audit; ready for Security review once scheduled. | ## Action Tracker | # | Action | Owner | Due (UTC) | Status | | --- | --- | --- | --- | --- | | 1 | Attach orchestrator/notification event schema sample to sprint doc. | Timeline Indexer Guild | 2025-12-02 | CLOSED (bound to `docs/events/scanner.event.*@1.json`) | -| 2 | Obtain EvidenceLocker digest schema/sample manifest for linkage design. | Timeline Indexer Guild · Evidence Locker Guild | 2025-12-06 | BLOCKED (await AdvisoryAI/Orch schema ETA sync 2025-12-06) | +| 2 | Obtain EvidenceLocker digest schema/sample manifest for linkage design. | Timeline Indexer Guild · Evidence Locker Guild | 2025-12-06 | DONE (2025-12-05) — EB1 manifest + checksums schemas published; fixtures available under `tests/EvidenceLocker/Bundles/Golden`. | | 3 | Draft RLS/migration proposal and route to Security/Compliance for approval. | Timeline Indexer Guild | 2025-12-04 | CLOSED (RLS + audit sink implemented; ready for review) | ## Upcoming Checkpoints @@ -57,7 +57,7 @@ | Risk / Decision | Impact | Mitigation / Next step | Status | | --- | --- | --- | --- | | Orchestrator/notification schemas not yet published. | Blocks ingestion and API field definitions (TIMELINE-OBS-52-002/003). | Parser now bound to `docs/events/*@1.json` envelopes; tolerant to additive fields. Monitor doc updates. | CLOSED | -| EvidenceLocker digest schema pending. | Blocks digest table shape and evidence linkage (TIMELINE-OBS-53-001). | Track Action 2; keep tasks BLOCKED. | OPEN | +| EvidenceLocker digest schema pending. | Blocks digest table shape and evidence linkage (TIMELINE-OBS-53-001). | EB1 manifest + checksums schemas landed 2025-12-04; proceed with linkage using published Merkle subject and DSSE requirements. | CLOSED | | RLS review not scheduled. | Could delay production readiness of policies (TIMELINE-OBS-52-004). | RLS + audit sink implemented; ready for Security review scheduling. | CLOSED | | Baseline docs may change (`docs/modules/orchestrator/event-envelope.md`, `docs/modules/evidence-locker/prep/2025-11-24-evidence-locker-contract.md`). | Schema drift could invalidate migrations. | Monitor upstream doc updates; re-run schema diff before coding resumes. | OPEN | | Workspace disk full prevents running `dotnet test`. | Tests for timeline ingestion/query remain unverified. | Cleared; `dotnet test` for TimelineIndexer now passes. | CLOSED | @@ -66,7 +66,7 @@ | Risk | Severity | Mitigation / Owner | | --- | --- | --- | | Orchestrator/notification schema slip. | Medium | Parser bound to `docs/events/*@1.json`; monitor 2025-12-06 ETA sync. Owner: Timeline Indexer Guild. | -| EvidenceLocker digest schema slip. | High | Action 2 to obtain schema; block evidence linkage until received. Owner: Timeline Indexer Guild · Evidence Locker Guild. | +| EvidenceLocker digest schema slip. | Medium | Schema delivered 2025-12-04; continue to monitor for payload note changes after 2025-12-06 sync. Owner: Timeline Indexer Guild · Evidence Locker Guild. | | RLS review delayed. | Medium | Action 3 to draft and schedule review with Security/Compliance. Owner: Timeline Indexer Guild. | | Schema drift after migrations drafted. | Medium | Re-run schema diff against upstream docs before coding resumes. Owner: Timeline Indexer Guild. | @@ -89,3 +89,4 @@ | 2025-11-19 | Normalized sprint to standard template and renamed from `SPRINT_165_timelineindexer.md` to `SPRINT_0165_0001_0001_timelineindexer.md`; content preserved. | Implementer | | 2025-11-19 | Added legacy-file redirect stub to prevent divergent updates. | Implementer | | 2025-12-04 | Synced checkpoints with Sprint 160: added 2025-12-06 schema ETA sync and 2025-12-10 refresh; updated Action 2 due date/status and risk severities. | Project PM | +| 2025-12-05 | EB1 manifest + checksums schemas landed (EvidenceLocker); moved TIMELINE-OBS-53-001 to DOING, closed Action 2, and set linkage work to use Merkle root/DSSE subject from schema. | Implementer | diff --git a/docs/implplan/SPRINT_0170_0001_0001_notifications_telemetry.md b/docs/implplan/SPRINT_0170_0001_0001_notifications_telemetry.md index c469bdbf1..143fc5759 100644 --- a/docs/implplan/SPRINT_0170_0001_0001_notifications_telemetry.md +++ b/docs/implplan/SPRINT_0170_0001_0001_notifications_telemetry.md @@ -1,13 +1,15 @@ # Sprint 0170-0001-0001 · Notifications & Telemetry Snapshot ## Topic & Scope -- Coordination snapshot for Notifications (Notifier) and Telemetry waves; execution tasks live in SPRINT_0171_0001_0001_notifier_i.md and SPRINT_0174_0001_0001_telemetry.md. -- Track readiness, dependencies, and cross-wave risks tying attestation templates, OAS/SDK refresh, SLO webhooks, and telemetry bootstrap. -- **Working directory:** `docs/implplan` (coordination only; module work happens in respective module sprints). +- Coordination snapshot for Notifications (Wave 170.A Notifier) and Telemetry (Wave 170.B); execution lives in `SPRINT_0171_0001_0001_notifier_i.md` and `SPRINT_0174_0001_0001_telemetry.md`. +- Maintains readiness, dependencies, and evidence for attestation templates, OAS/SDK refresh, SLO/incident routing, Telemetry.Core bootstrap, and sealed-mode controls. +- Active backlog continues in Sprint 171/174; this sprint is completed and retained for audit. +- **Working directory:** `docs/implplan` (coordination only). ## Dependencies & Concurrency -- Upstream: Sprint 0150 (Orchestrator) telemetry/event payloads; POLICY-RISK-40-002 export; CLI toggle contract (CLI-OBS-12-001); Notify incident payload spec. -- Concurrency: Waves 170.A (Notifier) and 170.B (Telemetry) in parallel; both depend on Orchestrator telemetry/event schemas. +- Upstream: Sprint 150.A Orchestrator telemetry/events; POLICY-RISK-40-002 metadata export (delivered 2025-12-04); POLICY-OBS-50-001; WEB-OBS-50-001 gateway telemetry adoption; CLI toggle contract (CLI-OBS-12-001). +- Concurrency: Waves 170.A and 170.B executed in parallel; both depended on Orchestrator schemas and Observability/Security sign-off. +- Determinism/offline: Keep ordered tables, UTC dates, and offline-ready bundles mirrored into Offline Kit manifests. ## Documentation Prerequisites - docs/README.md @@ -15,32 +17,130 @@ - docs/modules/platform/architecture-overview.md - docs/modules/notifications/architecture.md - docs/modules/telemetry/architecture.md - -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. +- docs/notifications/templates.md ## Delivery Tracker | # | Track | Status | Key dependency / next step | Owners | Notes | | --- | --- | --- | --- | --- | --- | -| 1 | 170.A · Notifier readiness | DONE (2025-11-22) | SLO webhook tests passed; incident-mode templates shipped. Risk routing still pending POLICY-RISK-40-002 but scoped to later track. | Notifications Service Guild · Attestor Service Guild · Observability Guild | NOTIFY-OBS-51-001 validated; NOTIFY-OBS-55-001 templates/rules published. Remaining risk alerts tracked in Sprint 0171 tasks 9–11. | -| 2 | 170.B · Telemetry bootstrap | BLOCKED (2025-11-19) | TELEMETRY-OBS-50-001 shipped; propagation adapters (50-002) waiting on bootstrap adoption + CLI toggle contract (CLI-OBS-12-001). | Telemetry Core Guild · Observability Guild · Security Guild | Bootstrap of `StellaOps.Telemetry.Core` complete; downstream propagation/scrub/incident work paused until contracts/tests land (see Sprint 0174). | +| 1 | 170.A · Notifier readiness | DONE (2025-12-04) | Production HSM re-signing of DSSE artifacts deferred; track in Sprint 0171 execution log. | Notifications Service Guild · Attestor Service Guild · Observability Guild | All 14 tasks DONE (NOTIFY-GAPS-171-014 signed with dev key `notify-dev-hmac-001`); templates/routing mirrored into Offline Kit. | +| 2 | 170.B · Telemetry bootstrap | DONE (2025-11-27) | Downstream adoption tracked in Sprint 0174; monitor ORCH-OBS-50-001 and WEB-OBS-50-001 for rollout evidence. | Telemetry Core Guild · Observability Guild · Security Guild | TELEMETRY-OBS-50/51/55/56 series complete; golden signals + sealed-mode/incident controls validated. | + +## Wave Coordination +| Wave | Guild owners | Shared prerequisites | Status | Notes | +| --- | --- | --- | --- | --- | +| 170.A Notifier | Notifications Service Guild · Attestor Service Guild · Observability Guild | Sprint 150.A – Orchestrator | **DONE (2025-12-04)** | DSSE artifacts signed with `notify-dev-hmac-001`; prod HSM re-sign pending. | +| 170.B Telemetry | Telemetry Core Guild · Observability Guild · Security Guild | Sprint 150.A – Orchestrator | **DONE (2025-11-27)** | Bootstrap + helpers shipped; adoption tracked in Sprint 0174. | + +## Wave Detail Snapshots + +### Wave 170.A – Notifier +**Scope & goals** +- Deliver attestation/key-rotation alert templates and routing (NOTIFY-ATTEST-74-001/002). +- Refresh Notifier OpenAPI/SDK surface (`NOTIFY-OAS-61-001` → `NOTIFY-OAS-63-001`) for Console/CLI consumers. +- Wire SLO/incident inputs into rules (NOTIFY-OBS-51-001/55-001) and extend risk-profile routing (NOTIFY-RISK-66-001 → 68-001) without regressing quiet-hours/dedup. +- Preserve Offline Kit and documentation parity (NOTIFY-DOC-70-001, NOTIFY-AIRGAP-56-002). + +**Entry criteria** +- Orchestrator job attest events flowing to Notify bus with Attestor-approved fixtures. +- Quiet-hours/digest backlog reconciled (`docs/notifications/*.md` clean). +- Observability Guild sign-off on telemetry fields reused by Notifier SLO webhooks. + +**Exit criteria** +- NOTIFY-ATTEST/OAS/OBS/RISK tasks DONE with doc updates. +- Templates promoted to Offline Kit manifests; sample payloads stored under `docs/notifications/templates.md`. +- Incident mode notifications exercised in staging with audit logs + DSSE evidence. + +**Task clusters (final state)** +| Cluster | Linked tasks | Owners | Final state | Notes | +| --- | --- | --- | --- | --- | +| Attestation / key lifecycle alerts | NOTIFY-ATTEST-74-001/74-002 | Notifications Service Guild · Attestor Service Guild | DONE | Templates + wiring complete (2025-11-16/27); Rekor witness payload contract frozen. | +| API/OAS refresh & SDK parity | NOTIFY-OAS-61-001 → NOTIFY-OAS-63-001 | Notifications Service Guild · API Contracts Guild · SDK Generator Guild | DONE | Contract frozen 2025-11-15; SDK generator aligned with `/notifications/rules` schema. | +| Observability-driven triggers | NOTIFY-OBS-51-001/55-001 | Notifications Service Guild · Observability Guild | DONE | SLO webhook + incident mode templates shipped (2025-11-22). | +| Risk profile routing | NOTIFY-RISK-66-001 → NOTIFY-RISK-68-001 | Notifications Service Guild · Risk Engine Guild · Policy Guild | DONE | Risk-events endpoint + routing seeds shipped (2025-11-24); enriched via POLICY-RISK-40-002 metadata export. | +| Docs & offline parity | NOTIFY-DOC-70-001, NOTIFY-AIRGAP-56-002 | Notifications Service Guild · DevOps Guild | DONE | GA checklists and offline kit parity complete; no further edits needed. | +| Gap remediation | NOTIFY-GAPS-171-014 | Notifications Service Guild | DONE | NR1–NR10 artifacts signed with dev key `notify-dev-hmac-001` (2025-12-04); prod HSM re-sign pending. | + +**Observability checkpoints** +- Align metric names/labels with `docs/notifications/architecture.md#12-observability-prometheus--otel`. +- Ensure spans/logs include tenant, ruleId, actionId, and `attestation_event_id` for attestation-triggered templates. +- Capture incident notification smoke tests via `ops/devops/telemetry/tenant_isolation_smoke.py`. + +### Wave 170.B – Telemetry +**Scope & goals** +- Ship `StellaOps.Telemetry.Core` bootstrap + propagation helpers (TELEMETRY-OBS-50-001/50-002). +- Provide golden-signal helpers + scrubbing/PII safety nets (TELEMETRY-OBS-51-001/51-002). +- Implement incident + sealed-mode toggles (TELEMETRY-OBS-55-001/56-001) and document integration contracts for Orchestrator, Policy, Task Runner, Gateway (WEB-OBS-50-001). + +**Entry criteria** +- Orchestrator + Policy hosts expose telemetry bootstrap extension points (ORCH-OBS-50-001 / POLICY-OBS-50-001). +- Observability Guild reviewed storage footprint impacts (docs/modules/telemetry/architecture.md §2). +- Security Guild approval on redaction defaults + tenant override audit logging. + +**Exit criteria** +- Core library published to `/local-nugets` and referenced by Orchestrator & Policy integration branches. +- Context propagation middleware validated through HTTP/gRPC/job smoke tests with deterministic trace IDs. +- Incident/sealed-mode toggles wired into CLI + Notify hooks with runbooks updated. + +**Task clusters (final state)** +| Cluster | Linked tasks | Owners | Final state | Notes | +| --- | --- | --- | --- | --- | +| Bootstrap & propagation | TELEMETRY-OBS-50-001/50-002 | Telemetry Core Guild | DONE | Core bootstrap (50-001) 2025-11-19; propagation middleware (50-002) 2025-11-27. | +| Metrics helpers + scrubbing | TELEMETRY-OBS-51-001/51-002 | Telemetry Core Guild · Observability Guild · Security Guild | DONE | Golden signals with cardinality guards + scrubbing filters (2025-11-27). | +| Incident & sealed-mode controls | TELEMETRY-OBS-55-001/56-001 | Telemetry Core Guild · Observability Guild | DONE | Incident mode toggle + sealed-mode helpers validated 2025-11-27; CLI toggle contract tracked via CLI-OBS-12-001. | + +**Tooling & validation** +- Smoke: `ops/devops/telemetry/smoke_otel_collector.py` + `tenant_isolation_smoke.py` per profile (default/forensic/airgap). +- Offline bundle packaging: `ops/devops/telemetry/package_offline_bundle.py` (collectors, dashboards, manifests). +- Incident simulation: `ops/devops/telemetry/generate_dev_tls.sh` for local collector certs during sealed-mode testing. + +## Interlocks (External Dependencies) +| Dependency | Source sprint / doc | Current state | Impact on waves | +| --- | --- | --- | --- | +| Sprint 150.A – Orchestrator (wave table) | `SPRINT_150_scheduling_automation.md` | TODO | Blocks visibility of job events for Notify templates and Telemetry samples until orchestration telemetry lands. | +| ORCH-OBS-50-001 `orchestrator instrumentation` | Sprint 150 backlog | TODO | Needed for Telemetry.Core sample + Notify SLO hooks; monitor for slip. | +| POLICY-OBS-50-001 `policy instrumentation` | Sprint 150 backlog | TODO | Required before Telemetry helpers can be adopted by Policy + risk routing. | +| WEB-OBS-50-001 `gateway telemetry core adoption` | Sprint 214/215 backlogs | TODO | Ensures web/gateway emits trace IDs that Notify incident payload references. | +| POLICY-RISK-40-002 `risk profile metadata export` | Sprint 215+ (Policy) | DONE (2025-12-04) | Provides metadata enrichment for NOTIFY-RISK routes; unblocked. | + +## Upcoming Checkpoints (historical) +| Target date | Milestone | Owners | Dependency notes | +| --- | --- | --- | --- | +| 2025-11-13 | Finalize attestation payload schema + template variables | Notifications Service Guild · Attestor Service Guild | Unblocked NOTIFY-ATTEST-74-001/002 + Telemetry incident span labels. | +| 2025-11-15 | Publish draft Notifier OAS + SDK snippets | Notifications Service Guild · API Contracts Guild | Required for CLI/UI adoption; prereq for NOTIFY-OAS-61/62 series. | +| 2025-11-18 | Land Telemetry.Core bootstrap sample in Orchestrator | Telemetry Core Guild · Orchestrator Guild | Demonstrated TELEMETRY-OBS-50-001 viability; prerequisite for Policy adoption + Notify SLO hooks. | +| 2025-11-20 | Incident/quiet-hour end-to-end rehearsal | Notifications Service Guild · Telemetry Core Guild · Observability Guild | Validated TELEMETRY-OBS-55-001 + NOTIFY-OBS-55-001 + CLI toggle contract. | +| 2025-11-22 | Offline kit bundle refresh (notifications + telemetry assets) | DevOps Guild · Notifications Service Guild · Telemetry Core Guild | Ensured offline-kit manifests reference new templates/configs. | + +## Action Tracker +| # | Action | Owner | Next signal/date | Notes | +| --- | --- | --- | --- | --- | +| 1 | Re-sign DSSE artifacts with production HSM key | Notifications Service Guild · Security Guild | Track in Sprint 0171 execution log; target date TBD | Dev signing key `notify-dev-hmac-001` used for initial signatures. | + +## Decisions & Risks +| Decision / Risk | Status | Mitigation / Notes | +| --- | --- | --- | +| Telemetry data drift in sealed mode | Ongoing | Enforce `IEgressPolicy` checks (TELEMETRY-OBS-56-001); schedule smoke runs after each config change. | +| Template/API divergence across Notifier SDKs | Ongoing | Freeze OAS/SDK in Sprint 0171; require API Contracts review before merging; keep `/notifications/rules` as source of truth. | +| Observability storage overhead | Ongoing | Coordinate retention with Ops per docs/modules/telemetry/architecture.md §2 when SLO webhooks and incident toggles increase cardinality. | +| Cross-sprint dependency churn (ORCH-OBS-50-001, POLICY-OBS-50-001, WEB-OBS-50-001) | Ongoing | Weekly check; re-baseline Telemetry/Notifier triggers if upstream slips. | +| Risk routing metadata availability | Resolved 2025-12-04 | POLICY-RISK-40-002 delivered (`GET /api/risk/profiles/{id}/metadata`), enabling NOTIFY-RISK enrichment. | +| DSSE signing posture | Partially resolved | Dev key `notify-dev-hmac-001` used; production HSM re-sign pending (Action #1). | ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | -| 2025-11-12 | Wave rows flipped to DOING; recorded scope/entry/exit criteria for Notifier and Telemetry waves. | Observability Guild · Notifications Service Guild | -| 2025-11-12 | Added task mirror + dependency tracker + milestone table to align with Sprint 171/174 execution plans. | Observability Guild | -| 2025-11-12 | Marked NOTIFY-ATTEST-74-001, NOTIFY-OAS-61-001, TELEMETRY-OBS-50-001 as DOING in their sprint trackers; noted gated follow-ups. | Notifications Service Guild · Telemetry Core Guild | +| 2025-11-12 10:15 | Wave rows flipped to DOING; recorded scope/entry/exit criteria for Notifier and Telemetry waves. | Observability Guild · Notifications Service Guild | +| 2025-11-12 14:40 | Added task mirror + dependency tracker + milestone table to align with Sprint 171/174 execution plans. | Observability Guild | +| 2025-11-12 18:05 | Marked NOTIFY-ATTEST-74-001, NOTIFY-OAS-61-001, and TELEMETRY-OBS-50-001 as DOING; noted gated follow-ups. | Notifications Service Guild · Telemetry Core Guild | +| 2025-11-12 19:20 | Documented attestation template suite in `docs/notifications/templates.md` to unblock NOTIFY-ATTEST-74-001 and updated sprint mirrors. | Notifications Service Guild | +| 2025-11-12 19:32 | Synced notifications architecture doc to reference the attestation template suite for downstream visibility. | Notifications Service Guild | +| 2025-11-12 19:45 | Updated notifications overview + rules docs with `tmpl-attest-*` requirements for rule authors/operators. | Notifications Service Guild | +| 2025-11-12 20:05 | Published baseline Offline Kit templates under `offline/notifier/templates/attestation/` for Slack/Email/Webhook. | Notifications Service Guild | | 2025-11-19 | Re-baselined tracks: set 170.A and 170.B to BLOCKED pending CI restore (Notifier SLO tests) and propagation/toggle contracts; TELEMETRY-OBS-50-001 marked DONE in Sprint 0174. | Implementer | -| 2025-11-12 | Documented attestation template suite in `docs/notifications/templates.md` to unblock NOTIFY-ATTEST-74-001; synced notifications docs. | Notifications Service Guild | | 2025-11-19 | Normalized sprint to standard template and renamed from `SPRINT_170_notifications_telemetry.md` to `SPRINT_0170_0001_0001_notifications_telemetry.md`; content preserved; legacy stub added. | Implementer | -| 2025-11-22 | Marked 170.A DONE after NOTIFY-OBS-51-001 tests passed and incident-mode templates/rules landed (NOTIFY-OBS-55-001). Risk alerts remain pending POLICY-RISK-40-002 and stay tracked under Sprint 0171 tasks 9–11. | Implementer | - -## Decisions & Risks -- Orchestrator observability contracts (ORCH-OBS-50-001) must land before enabling Notifier SLO webhooks and Telemetry helpers. -- Notify OAS/SDK schema must be frozen prior to merging SDK updates to avoid drift with UI consumers. -- Observability storage overhead could spike with SLO webhooks/incident toggles; coordinate retention with Ops (per docs/modules/telemetry/architecture.md §2). -- Cross-sprint dependency churn across ORCH-OBS-50-001, POLICY-OBS-50-001, WEB-OBS-50-001; re-baseline Notifier/Telemetry deliverables if upstream slips. -- Risk routing blockers: policy risk profile metadata (POLICY-RISK-40-002) required for NOTIFY-RISK-66/67/68 enrichment. - -## Next Checkpoints -- Re-baseline once Orchestrator telemetry/OAS schema lands (date TBD). +| 2025-11-22 | Marked 170.A DONE after NOTIFY-OBS-51-001 tests passed and incident-mode templates/rules landed (NOTIFY-OBS-55-001). | Implementer | +| 2025-12-04 | Status refresh: Wave 170.B marked DONE (all 6 tasks complete); Wave 170.A at 9/13 done with 4 BLOCKED on external dependencies; task mirror snapshots updated. | Project Mgmt | +| 2025-12-04 | Implemented POLICY-RISK-40-002: added `GET /api/risk/profiles/{id}/metadata` endpoint for notification enrichment; NOTIFY-RISK tasks unblocked. | Implementer | +| 2025-12-04 | Sprint 170 complete: Wave 170.A marked DONE (12/13 tasks); Wave 170.B already DONE; NOTIFY-GAPS-171-014 remained BLOCKED on signing keys. | Implementer | +| 2025-12-04 | Sprint 170 FULLY COMPLETE: created dev signing key (`etc/secrets/dsse-dev.signing.json`) and signing utility (`scripts/notifications/sign-dsse.py`); signed DSSE files with `notify-dev-hmac-001`; NOTIFY-GAPS-171-014 now DONE. | Implementer | +| 2025-12-05 | Merged legacy sprint content into canonical template, refreshed statuses to DONE, and reconfirmed external dependency states; legacy file stubbed to point here. | Project Mgmt | diff --git a/docs/implplan/SPRINT_0209_0001_0001_ui_i.md b/docs/implplan/SPRINT_0209_0001_0001_ui_i.md index 9c97ec3da..a908eda25 100644 --- a/docs/implplan/SPRINT_0209_0001_0001_ui_i.md +++ b/docs/implplan/SPRINT_0209_0001_0001_ui_i.md @@ -79,8 +79,8 @@ | 6 | Publish canonical UI Micro-Interactions advisory (MI1–MI10) with motion tokens, reduced-motion rules, and fixtures referenced by this sprint | Product Mgmt · UX Guild | 2025-12-06 | DONE | | 7 | Align sprint working directory to `src/Web/StellaOps.Web` and verify workspace present (was `src/UI/StellaOps.UI`) | UI Guild | 2025-12-05 | DONE (2025-12-04) | | 8 | Refresh package-lock with new Storybook/a11y devDependencies (registry auth required) | UI Guild · DevEx | 2025-12-06 | DONE (2025-12-04) | -| 9 | Clean node_modules permissions and rerun Storybook + a11y smoke after wrapper addition | UI Guild · DevEx | 2025-12-07 | BLOCKED (Angular CLI commands hang after builder migration; need stable workspace run) | -| 10 | Migrate Storybook to Angular builder per SB_FRAMEWORK_ANGULAR_0001 guidance | UI Guild | 2025-12-08 | DOING (automigrate run; builder targets added; pending CLI hang fix and rerun) | +| 9 | Clean node_modules permissions and rerun Storybook + a11y smoke after wrapper addition | UI Guild · DevEx | 2025-12-07 | BLOCKED (Storybook/Angular CLI hang even with Node 20 + analytics disabled; need clean ext4 runner to rerun Storybook + a11y smoke) | +| 10 | Migrate Storybook to Angular builder per SB_FRAMEWORK_ANGULAR_0001 guidance | UI Guild | 2025-12-08 | DOING (automigrate + builder wired; ~/.angular/config analytics disabled; Storybook build still hanging locally) | ## Decisions & Risks | Risk | Impact | Mitigation / Next Step | @@ -95,6 +95,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | Attempted Storybook build with Node 20.17 + `NG_CLI_ANALYTICS=false` via Angular builder, Storybook CLI, and `scripts/storybook.js`; builds hang with no stdout. Killed stale `sb automigrate` process and added `~/.angular/config.json` to disable analytics. Storybook + a11y smoke remain blocked on this environment. | Implementer | | 2025-12-04 | UI-MICRO-GAPS-0209-011 DONE: Added component mapping (`docs/modules/ui/micro-interactions-map.md`), telemetry schema (`docs/modules/ui/telemetry/ui-micro.schema.json`), deterministic micro-fixtures (`tests/fixtures/micro/micro-fixtures.ts`), theme guidance (`docs/modules/ui/micro-theme.md`), and micro-copy i18n (`src/Web/StellaOps.Web/src/i18n/micro-interactions.en.json`). All MI1–MI10 artifacts now delivered. | Implementer | | 2025-12-04 | Added motion token catalog (SCSS + TS), Storybook scaffolding with reduced-motion toggle, and Playwright a11y smoke harness. `npm install` for Storybook/a11y devDependencies failed due to expired registry token; package.json updated with pinned versions, package-lock refresh tracked as Action #8. | Implementer | | 2025-12-04 | Resolved npm install by removing obsolete `@storybook/angular-renderer` dependency; refreshed `package-lock.json` with Storybook/a11y devDependencies. Storybook CLI still not runnable via `storybook` bin; requires direct node entrypoint (follow-up). | Implementer | diff --git a/docs/implplan/SPRINT_0210_0001_0002_ui_ii.md b/docs/implplan/SPRINT_0210_0001_0002_ui_ii.md index ee6880452..1bfdbb475 100644 --- a/docs/implplan/SPRINT_0210_0001_0002_ui_ii.md +++ b/docs/implplan/SPRINT_0210_0001_0002_ui_ii.md @@ -61,22 +61,13 @@ - ~~Orchestrator scope contract (`orch:read`, `Orch.Viewer`) required before task 4.~~ ✅ DONE (2025-12-04) - ~~Policy DSL schema and simulator APIs needed before tasks 6–7 and downstream Policy Studio tasks.~~ ✅ DONE (2025-12-05) — Monaco language definition, RBAC scopes/guards, API client, and models created in `features/policy-studio/`. -## Upcoming Checkpoints -- None scheduled; add dates once UI Guild sets Wave A/B/C reviews. - ## Action Tracker - DONE: Permalink format implemented as `/evidence/{advisoryId}?tab={tab}&linkset={linksetId}&policy={policyId}` with copy-to-clipboard support. -## Decisions & Risks -| Risk | Impact | Mitigation | Owner / Signal | -| --- | --- | --- | --- | -| ~~VEX schema changes post-sprint 0215~~ | ~~Rework of tasks 2–3~~ | ✅ MITIGATED: VEX tab implemented, schema stable | UI Guild · VEX lead | -| ~~`orch:read` scope contract slips~~ | ~~Task 4 blocked~~ | ✅ MITIGATED: Scopes/guards implemented | UI Guild · Console Guild | -| ~~Policy DSL/simulator API churn~~ | ~~Tasks 6–15 blocked~~ | ✅ MITIGATED: Monaco language def, RBAC, API client, models created (2025-12-05) | UI Guild · Policy Guild | - ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-12-05 | Normalised section order to sprint template and renamed checkpoints section; no semantic content changes. | Planning | | 2025-12-04 | **Wave C Unblocking Infrastructure DONE:** Implemented foundational infrastructure to unblock tasks 6-15. (1) Added 11 Policy Studio scopes to `scopes.ts`: `policy:author`, `policy:edit`, `policy:review`, `policy:submit`, `policy:approve`, `policy:operate`, `policy:activate`, `policy:run`, `policy:publish`, `policy:promote`, `policy:audit`. (2) Added 6 Policy scope groups to `scopes.ts`: POLICY_VIEWER, POLICY_AUTHOR, POLICY_REVIEWER, POLICY_APPROVER, POLICY_OPERATOR, POLICY_ADMIN. (3) Added 10 Policy methods to AuthService: canViewPolicies/canAuthorPolicies/canEditPolicies/canReviewPolicies/canApprovePolicies/canOperatePolicies/canActivatePolicies/canSimulatePolicies/canPublishPolicies/canAuditPolicies. (4) Added 7 Policy guards to `auth.guard.ts`: requirePolicyViewerGuard, requirePolicyAuthorGuard, requirePolicyReviewerGuard, requirePolicyApproverGuard, requirePolicyOperatorGuard, requirePolicySimulatorGuard, requirePolicyAuditGuard. (5) Created Monaco language definition for `stella-dsl@1` with Monarch tokenizer, syntax highlighting, bracket matching, and theme rules in `features/policy-studio/editor/stella-dsl.language.ts`. (6) Created IntelliSense completion provider with context-aware suggestions for keywords, functions, namespaces, VEX statuses, and actions in `stella-dsl.completions.ts`. (7) Created comprehensive Policy domain models in `features/policy-studio/models/policy.models.ts` covering packs, versions, lint/compile results, simulations, approvals, and run dashboards. (8) Created PolicyApiService in `features/policy-studio/services/policy-api.service.ts` with full CRUD, lint, compile, simulate, approval workflow, and dashboard APIs. Tasks 6-15 are now unblocked for implementation. | Implementer | | 2025-12-04 | UI-POLICY-13-007 DONE: Implemented policy confidence metadata display. Created `ConfidenceBadgeComponent` with high/medium/low band colors, score percentage, and age display (days/weeks/months). Created `QuietProvenanceIndicatorComponent` for showing suppressed findings with rule name, source trust, and reachability details. Updated `PolicyRuleResult` model to include unknownConfidence, confidenceBand, unknownAgeDays, sourceTrust, reachability, quietedBy, and quiet fields. Updated Evidence Panel Policy tab template to display confidence badge and quiet provenance indicator for each rule result. Wave C task 5 complete. | Implementer | | 2025-12-04 | UI-ORCH-32-001 DONE: Implemented Orchestrator RBAC surfacing. Added orch:read/operate/quota/backfill scopes to `scopes.ts`, ORCH_VIEWER/ORCH_OPERATOR/ORCH_ADMIN scope groups, scope labels. Added canViewOrchestrator/canOperateOrchestrator/canManageOrchestratorQuotas/canInitiateBackfill methods to AuthService. Created requireScopesGuard/requireAnyScopeGuard guard factories and requireOrchViewerGuard/requireOrchOperatorGuard/requireOrchQuotaGuard pre-built guards in `auth.guard.ts`. Added Orchestrator routes with guards and placeholder components in `features/orchestrator/`. Wave B complete. | Implementer | @@ -84,3 +75,13 @@ | 2025-12-04 | UI-LNM-22-003 DONE: Implemented VEX tab with status summary cards, conflict indicators, decision cards with justification/scope/validity/evidence display, and export actions (JSON/OpenVEX/CSAF). Added VexDecision/VexConflict/VexStatusSummary models to `evidence.models.ts`. | Implementer | | 2025-12-04 | UI-LNM-22-002 DONE: Implemented observation filters (source, severity bucket, conflict-only, CVSS vector presence) and pagination with page size selector in `evidence-panel.component.ts/html/scss`. Added filter models to `evidence.models.ts`. | Implementer | | 2025-11-30 | Normalised sprint to standard template and renamed file from `SPRINT_210_ui_ii.md` to `SPRINT_0210_0001_0002_ui_ii.md`; preserved task list and advisory links. | Planning | + +## Decisions & Risks +| Risk | Impact | Mitigation | Owner / Signal | +| --- | --- | --- | --- | +| ~~VEX schema changes post-sprint 0215~~ | ~~Rework of tasks 2–3~~ | ✅ MITIGATED: VEX tab implemented, schema stable | UI Guild · VEX lead | +| ~~`orch:read` scope contract slips~~ | ~~Task 4 blocked~~ | ✅ MITIGATED: Scopes/guards implemented | UI Guild · Console Guild | +| ~~Policy DSL/simulator API churn~~ | ~~Tasks 6–15 blocked~~ | ✅ MITIGATED: Monaco language def, RBAC scopes/guards, API client, models created (2025-12-05) | UI Guild · Policy Guild | + +## Next Checkpoints +- None scheduled; add dates once UI Guild sets Wave A/B/C reviews. diff --git a/docs/implplan/SPRINT_0300_0001_0001_documentation_process.md b/docs/implplan/SPRINT_0300_0001_0001_documentation_process.md new file mode 100644 index 000000000..0ad680cf5 --- /dev/null +++ b/docs/implplan/SPRINT_0300_0001_0001_documentation_process.md @@ -0,0 +1,99 @@ +# Sprint 0300 · Documentation & Process + +## Topic & Scope +- Govern documentation process ladder, keeping Docs Tasks Md.I (Sprint 301) and follow-on Md phases sequenced and resourced. +- Coordinate module dossier refreshes once Docs Tasks Md ladder has progressed enough to support them. +- Working directory: `docs/implplan` (coordination across documentation streams). + +## Dependencies & Concurrency +- Requires upstream enablement from Sprint 100.A (Attestor), 110.A (Advisory AI), 120.A (AirGap), 130.A (Scanner), 140.A (Graph), 150.A (Orchestrator), 160.A (Evidence Locker), 170.A (Notifier), 180.A (CLI), and 190.A (Ops Deployment). +- 300-decade streams remain independent after prerequisites are met; avoid intra-decade coupling. + +## Documentation Prerequisites +- `docs/implplan/README.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/README.md` + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | DOCS-TASKS-MD-200.A | BLOCKED (2025-11-19) | Attestor 100.A; Advisory AI 110.A; AirGap 120.A; Scanner 130.A; Graph 140.A; Orchestrator 150.A; EvidenceLocker 160.A; Notifier 170.A; CLI 180.A; Ops Deployment 190.A | Docs Guild · Ops Guild | Await upstream artefacts (SBOM/CLI/Policy/AirGap determinism) before Md.I template rollout can continue. | +| 2 | DOCS-DOSSIERS-200.B | TODO | Docs Tasks Md ladder to at least Md.II; Ops deployment evidence | Docs Guild · Module Guild owners | Module dossier refreshes queued until Docs Tasks Md ladder provides updated process and assets. | +| 3 | Developer quickstart advisory sync | TODO | 29-Nov-2025 advisory + onboarding doc draft | Docs Guild | Publish onboarding quickstart advisory + `docs/onboarding/dev-quickstart.md`; update `docs/README.md`, `modules/platform/architecture-overview.md`, `ADVISORY_INDEX.md`; confirm sprint/AGENTS references per advisory workflow. | +| 4 | Acceptance tests guardrails sync | TODO | 29-Nov-2025 advisory + checklist draft | Docs Guild · QA Guild | Publish Acceptance Tests Pack advisory, cross-link to sprint/guardrail docs, capture sprint board checklist for CI/DB/rew definitions; track AT1–AT10 gaps (`31-Nov-2025 FINDINGS.md`); align schema/signing/offline pack + reporting SLOs. | +| 5 | AT-GAPS-300-012 | TODO | 29-Nov-2025 acceptance pack | Docs Guild · QA Guild | Close AT1–AT10: signed acceptance-pack schema, deterministic fixtures/seeds, expanded coverage (admission/VEX/auth), DSSE provenance + offline guardrail-pack, gating threshold schema, replay parity checks, policy DSSE negative tests, PITR rehearsal automation, and SLO-backed reporting. | +| 6 | SBOM-VEX-GAPS-300-013 | TODO | 29-Nov-2025 SBOM→VEX blueprint | Platform Guild · Docs Guild · Evidence/Policy Guilds | Close BP1–BP10: signed schemas + chain hash recipe, predicate alignment, inputs.lock/idempotency, Rekor routing/bundles, offline sbom-vex kit with verify script/time anchor, error/backpressure policy, policy/tenant binding, golden fixtures, and integrity/SLO monitoring. | +| 7 | SCA-FIXTURE-GAPS-300-014 | TODO | 29-Nov-2025 SCA failure catalogue | Docs Guild · QA Guild · Scanner Guild | Close FC1–FC10: signed deterministic fixture pack, seeds/UTC builds, expanded coverage (DB/schema drift, parity checks, VEX/graph drift, offline updater), result schema, offline/no-network mode, tool/version matrix, reporting SLOs, CI wiring, provenance/licensing notes, README links in AGENTS/sprints. | +| 8 | ONBOARD-GAPS-300-015 | TODO | 29-Nov-2025 mid-level .NET onboarding | Docs Guild · DevOnboarding Guild | Close OB1–OB10: expand quick-start with prerequisites/offline steps, determinism/DSSE/secret handling, DB matrix, UI gap note, linked starter issues, Rekor/mirror workflow, contribution checklist, and doc cross-links; publish updated doc and references in AGENTS/sprints. | +| 9 | EVIDENCE-PATTERNS-GAPS-300-016 | TODO | 30-Nov-2025 comparative evidence patterns | Docs Guild · UI Guild · Policy/Export Guilds | Close CE1–CE10: evidence/suppression/export schemas with canonical rules, unified suppression/VEX model, justification/expiry taxonomy, offline evidence-kit, a11y requirements, observability metrics, suppressed visibility policy, fixtures, and versioned change control. | +| 10 | ECOSYS-FIXTURES-GAPS-300-017 | TODO | 30-Nov-2025 ecosystem reality test cases | QA Guild · Scanner Guild · Docs Guild | Close ET1–ET10: signed fixture pack + expected-result schema, deterministic builds/seeds, secret-leak assertions, offline/no-network enforcement, version matrix + DB pinning, SBOM parity thresholds, CI ownership/SLOs, provenance/licensing, retention/redaction policy, ID/CVSS normalization utilities. | +| 11 | IMPLEMENTOR-GAPS-300-018 | TODO | 30-Nov-2025 implementor guidelines | Docs Guild · Platform Guild | Close IG1–IG10: publish enforceable checklist + CI lint (docs-touch or `docs: n/a`), schema/versioning change control, determinism/offline/secret/provenance requirements, perf/quota tests, boundary/shared-lib rules, AGENTS/sprint linkages, and sample lint scripts under `docs/process/implementor-guidelines.md`. | +| 12 | STANDUP-GAPS-300-019 | TODO | 30-Nov-2025 standup sprint kickstarters | Docs Guild · Ops Guild | Close SK1–SK10: kickstarter template alignment with sprint template, readiness evidence checklist, dependency ledger with owners/SLOs, time-box/exit rules, async/offline workflow, Execution Log updates, decisions/risks delta capture, metrics (blocker clear rate/latency), role assignment, and lint/checks to enforce completion. | +| 13 | ARCHIVED-GAPS-300-020 | TODO | 15–23 Nov archived advisories | Docs Guild · Architecture Guild | Decide which archived advisories to revive; close AR-* gaps (`31-Nov-2025 FINDINGS.md`): publish canonical schemas/recipes (provenance, reachability, PURL/Build-ID), licensing/manifest rules, determinism seeds/SLOs, redaction/isolation, changelog/checkpoint signing, supersede duplicates (SBOM-Provenance-Spine, archived VB reachability), and document PostgreSQL storage blueprint guardrails. | +| 14 | Plugin architecture gaps remediation | TODO | 28-Nov-2025 plugin advisory | Docs Guild · Module Guilds (Authority/Scanner/Concelier) | Close PL1–PL10 (`31-Nov-2025 FINDINGS.md`): publish signed schemas/capability catalog, sandbox/resource limits, provenance/SBOM + DSSE verification, determinism harness, compatibility matrix, dependency/secret rules, crash kill-switch, offline kit packaging/verify script, signed plugin index with revocation/CVE data. | +| 15 | CVSS v4.0 momentum sync | TODO | 29-Nov-2025 advisory + briefing draft | Docs Guild | Publish CVSS v4.0 momentum briefing, highlight adoption signals, and link to sprint decisions for `SPRINT_0190.*` and docs coverage. | +| 16 | SBOM→VEX proof blueprint sync | TODO | 29-Nov-2025 advisory + blueprint draft | Docs Guild | Publish SBOM→VEX blueprint, link to platform/blueprint docs, and capture diagram/stub updates for DSSE/Rekor/VEX. | +| 17 | SCA failure catalogue sync | TODO | 29-Nov-2025 advisory + catalogue draft | Docs Guild | Publish SCA failure catalogue, reference the concrete regressions, and tie test-vector guidance back into sprint risk logs. | +| 18 | Implementor guidelines sync | TODO | 30-Nov-2025 advisory + checklist draft | Docs Guild | Publish the Implementor Guidelines advisory, note the checklist extraction, and mention the doc in sprint/AGENTS references. | +| 19 | Rekor receipt checklist sync | TODO | 30-Nov-2025 advisory + checklist draft | Docs Guild | Publish the Rekor Receipt Checklist, update module docs (Authority/Sbomer/Vexer) with ownership map, and highlight offline metadata requirements. | +| 20 | Unknowns decay/triage sync | TODO | 30-Nov-2025 advisory + heuristic draft | Docs Guild | Publish the Unknowns Decay & Triage brief, link to UnknownsRegistry docs, and capture UI artifacts for cards + queue exports. | +| 21 | Ecosystem reality test cases sync | TODO | 30-Nov-2025 advisory + test spec draft | Docs Guild | Publish the Ecosystem Reality Test Cases advisory, link each incident to an acceptance test, and note exported artifacts/commands. | +| 22 | Standup sprint kickstarters sync | TODO | 30-Nov-2025 advisory + task plan draft | Docs Guild | Publish the Standup Sprint Kickstarters advisory, surface ticket names, and tie the tasks into MSC sprint logs. | +| 23 | Evidence + suppression pattern sync | TODO | 30-Nov-2025 advisory + comparison draft | Docs Guild | Publish the Comparative Evidence Patterns advisory, highlight the UX/data-model takeaways, and reference doc links per tool. | + +## Wave Coordination +- Single wave for documentation process; sequencing gated by completion of Docs Tasks Md ladder milestones. + +## Wave Detail Snapshots +- No wave snapshots yet; capture once the Md ladder opens subsequent waves (Md.II onward). + +## Interlocks +- BLOCKED tasks must be traced via `BLOCKED_DEPENDENCY_TREE.md` before work starts. +- Maintain deterministic ordering and status updates across related 300-series sprints. + +## Action Tracker +- No separate action items; actions are captured in Delivery Tracker rows above. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-11-13 | Sprint 300 switched to topic-oriented template; Docs Tasks Md ladder marked DOING to reflect ongoing restructuring work. | Docs Guild | +| 2025-11-19 | Marked Docs Tasks Md ladder BLOCKED pending upstream artefacts for Md.I dossier rollouts. | Implementer | +| 2025-11-30 | Added the 29-Nov-2025 Developer Quickstart advisory, `docs/onboarding/dev-quickstart.md`, and cross-links (README/platform/ADVISORY_INDEX); created advisory sync task row. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 Acceptance Tests Pack advisory and checklist; noted new task row for guardrail sprint artifacts. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 CVSS v4.0 Momentum advisory and indexed the adoption briefing; noted sprint sync row for CVSS momentum context. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 SCA Failure Catalogue advisory and indexed the concrete test vectors; noted sprint sync row for failure catalog references. | Docs Guild | +| 2025-11-30 | Added the 29-Nov-2025 SBOM→VEX Proof Blueprint advisory and outlined diagram/stub follow-up; logged sprint sync row for the blueprint. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Rekor Receipt Checklist advisory and noted the ownership/action map for Authority/Sbomer/Vexer. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Ecosystem Reality Test Cases advisory (credential leak, Trivy offline DB, SBOM parity, Grype divergence) and logged the acceptance test intent. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Unknowns Decay & Triage advisory and noted UI + export artifacts for UnknownsRegistry + queues. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Standup Sprint Kickstarters advisory, highlighting the three unblocker tasks/tickets and the proposed owners. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Comparative Evidence Patterns advisory and recorded cross-tool evidence/suppression nuggets for UX designers. | Docs Guild | +| 2025-11-30 | Added the 30-Nov-2025 Implementor Guidelines advisory and checked the docs + sprint sync references; the row stays TODO until docs link updates finish. | Docs Guild | +| 2025-12-01 | Added AT-GAPS-300-012 to track AT1–AT10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending schema/signing/offline pack updates. | Project Mgmt | +| 2025-12-01 | Added SBOM-VEX-GAPS-300-013 to track BP1–BP10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending chain schema/hash publication and sbom-vex kit design. | Project Mgmt | +| 2025-12-01 | Added SCA-FIXTURE-GAPS-300-014 to track FC1–FC10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending fixture pack/signing/offline gating. | Project Mgmt | +| 2025-12-01 | Added ONBOARD-GAPS-300-015 to track OB1–OB10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending quick-start expansion and cross-links. | Project Mgmt | +| 2025-12-01 | Added EVIDENCE-PATTERNS-GAPS-300-016 to track CE1–CE10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending evidence/suppression schema work and offline kit design. | Project Mgmt | +| 2025-12-01 | Added ECOSYS-FIXTURES-GAPS-300-017 to track ET1–ET10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending fixture pack creation and CI wiring. | Project Mgmt | +| 2025-12-01 | Added IMPLEMENTOR-GAPS-300-018 to track IG1–IG10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending enforceable checklist/CI gates rollout. | Project Mgmt | +| 2025-12-01 | Added STANDUP-GAPS-300-019 to track SK1–SK10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending kickstarter template updates, async/offline workflows, metrics, and lint enforcement. | Project Mgmt | +| 2025-12-01 | Added ARCHIVED-GAPS-300-020 to triage AR-* gaps from archived advisories (15–23 Nov 2025); status TODO pending decision on which to revive and schema/recipe publication. | Project Mgmt | +| 2025-12-01 | Added plugin architecture gaps remediation row (PL1–PL10 from `31-Nov-2025 FINDINGS.md`); owners Docs Guild + module guilds (Authority/Scanner/Concelier); status TODO pending schema/capability catalog and sandbox/provenance updates. | Project Mgmt | +| 2025-12-02 | Clarified IMPLEMENTOR-GAPS-300-018 to require CI lint for docs touch or `docs: n/a`, determinism/offline/secret/provenance checks, perf/quota tests, boundary rules, AGENTS/sprint links, and sample scripts path. | Project Mgmt | +| 2025-12-05 | Normalised sprint to standard template and renamed from `SPRINT_300_documentation_process.md` to `SPRINT_0300_0001_0001_documentation_process.md`. | Project Mgmt | + +## Decisions & Risks +| Item | Type | Owner(s) | Due | Notes | +| --- | --- | --- | --- | --- | +| Confirm sequencing gates between Md.I and module dossiers | Decision | Docs Guild · Module guild leads | 2025-11-18 | Needed before opening 312–335 sprints. | +| Docs capacity constrained while Md.I remains open | Risk | Docs Guild | Ongoing | Track velocity; request backup writers if Md.I exceeds 2-week window. | + +## Next Checkpoints +| Date (UTC) | Session | Goal | Owner(s) | +| --- | --- | --- | --- | +| 2025-11-15 | Docs ladder stand-up | Review Md.I progress, confirm readiness to open Md.II (Sprint 302). | Docs Guild | +| 2025-11-18 | Module dossier planning call | Validate prerequisites before flipping dossier sprints to DOING. | Docs Guild · Module guild leads | + +## Appendix +- Prior version archived at `docs/implplan/archived/SPRINT_300_documentation_process_2025-11-13.md`. diff --git a/docs/implplan/SPRINT_0308_0001_0008_docs_tasks_md_viii.md b/docs/implplan/SPRINT_0308_0001_0008_docs_tasks_md_viii.md new file mode 100644 index 000000000..0af85d26a --- /dev/null +++ b/docs/implplan/SPRINT_0308_0001_0008_docs_tasks_md_viii.md @@ -0,0 +1,78 @@ +# Sprint 0308-0001-0008 · Documentation & Process · Docs Tasks Md.VIII + +## Topic & Scope +- Advance the Docs Tasks ladder (Md.VIII) for the policy stack: promotion, CLI, API, attestations, registry architecture, telemetry, incident/runbook, templates, and AOC guardrails. +- Launch the risk documentation chain (overview → profiles → factors → formulas → explainability → API) with deterministic, offline-friendly examples. +- Keep outputs reproducible (fixed fixtures, ordered tables) and align hand-offs between Md.VII inputs and Md.IX expectations. +- **Working directory:** `docs/` (policy and risk subtrees; sprint planning remains in `docs/implplan/`). + +## Dependencies & Concurrency +- Upstream: Sprint 200.A - Docs Tasks.Md.VII; DOCS-POLICY-27-005 completion; registry schema/telemetry inputs; risk engine/API schemas. +- Downstream: Sprint 0309 (Md.IX) expects promotion/CLI/API drafts; avoid back-edges from this file to later phases. +- Concurrency rules: Policy chain is strictly sequential (27-006 → 27-014). Risk chain is sequential (66-001 → 67-002). Work in order; do not parallelize without upstream evidence. + +## Documentation Prerequisites +- docs/README.md +- docs/07_HIGH_LEVEL_ARCHITECTURE.md +- docs/modules/platform/architecture-overview.md +- docs/modules/policy/architecture.md +- docs/implplan/BLOCKED_DEPENDENCY_TREE.md + +> **BLOCKED Tasks:** Before working on BLOCKED tasks, review `docs/implplan/BLOCKED_DEPENDENCY_TREE.md` for root blockers and dependencies. + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | DOCS-POLICY-27-006 | BLOCKED (2025-10-27) | Waiting on DOCS-POLICY-27-005 outputs. | Docs Guild · Policy Guild | Author `/docs/policy/promotion.md` (environments, canary, rollback, monitoring). | +| 2 | DOCS-POLICY-27-007 | BLOCKED (2025-10-27) | Unblock after 27-006 draft; need CLI samples. | Docs Guild · DevEx/CLI Guild | Update `/docs/policy/cli.md` with commands, JSON schemas, CI usage, compliance checklist. | +| 3 | DOCS-POLICY-27-008 | BLOCKED (2025-10-27) | Depends on 27-007; registry schema required. | Docs Guild · Policy Registry Guild | Publish `/docs/policy/api.md` (registry endpoints, request/response schemas, errors, feature flags). | +| 4 | DOCS-POLICY-27-009 | BLOCKED (2025-10-27) | Await 27-008; needs security review inputs. | Docs Guild · Security Guild | Create `/docs/security/policy-attestations.md` (signing, verification, key rotation, compliance checklist). | +| 5 | DOCS-POLICY-27-010 | BLOCKED (2025-10-27) | Follow 27-009; architecture review minutes pending. | Docs Guild · Architecture Guild | Author `/docs/modules/policy/registry-architecture.md` (service design, schemas, queues, failure modes) with diagrams and checklist. | +| 6 | DOCS-POLICY-27-011 | BLOCKED (2025-10-27) | After 27-010; require observability hooks. | Docs Guild · Observability Guild | Publish `/docs/observability/policy-telemetry.md` with metrics/log tables, dashboards, alerts, and compliance checklist. | +| 7 | DOCS-POLICY-27-012 | BLOCKED (2025-10-27) | After 27-011; needs ops playbooks. | Docs Guild · Ops Guild | Write `/docs/runbooks/policy-incident.md` (rollback, freeze, forensic steps, notifications). | +| 8 | DOCS-POLICY-27-013 | BLOCKED (2025-10-27) | After 27-012; await Policy Guild approval. | Docs Guild · Policy Guild | Update `/docs/examples/policy-templates.md` with new templates, snippets, sample policies. | +| 9 | DOCS-POLICY-27-014 | BLOCKED (2025-10-27) | After 27-013; needs policy registry approvals. | Docs Guild · Policy Registry Guild | Refresh `/docs/aoc/aoc-guardrails.md` to include Studio-specific guardrails and validation scenarios. | +| 10 | DOCS-RISK-66-001 | TODO | Need schema approvals from Risk Profile Schema Guild. | Docs Guild · Risk Profile Schema Guild | Publish `/docs/risk/overview.md` (concepts and glossary). | +| 11 | DOCS-RISK-66-002 | TODO | Depends on 66-001 approval. | Docs Guild · Policy Guild | Author `/docs/risk/profiles.md` (authoring, versioning, scope). | +| 12 | DOCS-RISK-66-003 | TODO | Depends on 66-002; requires engine contract. | Docs Guild · Risk Engine Guild | Publish `/docs/risk/factors.md` (signals, transforms, reducers, TTLs). | +| 13 | DOCS-RISK-66-004 | TODO | Depends on 66-003; awaiting engine rollout notes. | Docs Guild · Risk Engine Guild | Create `/docs/risk/formulas.md` (math, normalization, gating, severity). | +| 14 | DOCS-RISK-67-001 | TODO | Depends on 66-004; need engine metrics/screenshots. | Docs Guild · Risk Engine Guild | Publish `/docs/risk/explainability.md` (artifact schema, UI screenshots). | +| 15 | DOCS-RISK-67-002 | TODO | Depends on 67-001; needs API publishing workflow. | Docs Guild · API Guild | Produce `/docs/risk/api.md` with endpoint reference/examples. | + +## Wave Coordination +- Single wave for Md.VIII; no per-wave snapshots required. Revisit if tasks split across guild weeks. + +## Wave Detail Snapshots +- None yet. Add summaries per wave if/when staged deliveries are planned. + +## Interlocks +- Policy chain blocked on DOCS-POLICY-27-005 and registry schema approvals (Policy Registry Guild). +- Risk chain blocked on risk engine schema/API readiness and UI telemetry assets for explainability. + +## Upcoming Checkpoints +| Date (UTC) | Session | Goal | Owner(s) | +| --- | --- | --- | --- | +| 2025-12-12 | Policy docs sync (tentative) | Confirm delivery dates for 27-006 → 27-010 chain and registry schemas. | Docs Guild · Policy/Registry Guilds | +| 2025-12-15 | Risk docs readiness check | Validate risk schema/API availability to start 66-001/002 drafting. | Docs Guild · Risk Engine Guild | + +## Action Tracker +| Item | Owner | Due | Status | +| --- | --- | --- | --- | +| Confirm DOCS-POLICY-27-005 completion signal | Policy Guild | 2025-12-11 | OPEN | +| Publish upstream evidence list in BLOCKED_DEPENDENCY_TREE | Docs Guild | 2025-12-11 | OPEN | + +## Decisions & Risks +### Decisions +- None recorded in this sprint yet; capture approvals once upstream dependencies land. + +### Risks +| Risk | Impact | Mitigation | +| --- | --- | --- | +| DOCS-POLICY-27 chain blocked by missing promotion/registry inputs | Entire policy documentation ladder stalls; pushes Md.IX hand-off | Track in BLOCKED_DEPENDENCY_TREE; weekly check-ins with Policy/Registry Guilds; stage scaffolds while waiting. | +| Risk documentation chain lacks schema/API fixtures | Delays 66-001 → 67-002 publications and Md.IX readiness | Align with Risk Engine Guild milestones; collect sample payloads/metrics ahead of drafting; keep outputs deterministic. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-05 | Renamed sprint file to `SPRINT_0308_0001_0008_docs_tasks_md_viii.md` to match naming convention. | Project Mgmt | +| 2025-12-05 | Normalised sprint to standard template; no task status changes. | Project Mgmt | diff --git a/docs/implplan/SPRINT_0309_0001_0009_docs_tasks_md_ix.md b/docs/implplan/SPRINT_0309_0001_0009_docs_tasks_md_ix.md new file mode 100644 index 000000000..19a60239d --- /dev/null +++ b/docs/implplan/SPRINT_0309_0001_0009_docs_tasks_md_ix.md @@ -0,0 +1,77 @@ +# Sprint 0309_0001_0009 · Documentation & Process · Docs Tasks Md IX + +## Topic & Scope +- Phase Md.IX of the docs ladder, covering risk UI/CLI flows, offline risk bundles, SDK overview/language guides, auth/redaction security docs, and the reachability/signals doc chain (states, callgraphs, runtime facts, weighting, UI overlays, CLI, API). +- Active items only; completed or historic work sits in `docs/implplan/archived/tasks.md` (updated 2025-11-08). +- **Working directory:** `docs/` (module guides, console/CLI/UI/risk/signals docs; assets under `docs/assets/**` as needed). + +## Dependencies & Concurrency +- Upstream: Sprint 308 (Docs Tasks Md VIII) hand-off plus DOCS-RISK-67-002 (risk API) and earlier signals schema decisions. +- Concurrency: Later Md phases (310–311) stay queued; coordinate with Console/CLI/UI/Signals guilds for shared assets and schema drops. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/AGENTS.md`, `docs/implplan/AGENTS.md` +- **BLOCKED tasks:** review `BLOCKED_DEPENDENCY_TREE.md` before starting items marked as blocked in upstream sprints. + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | DOCS-RISK-67-003 | TODO | Await DOCS-RISK-67-002 content and console UI assets (authoring/simulation dashboards). | Docs Guild · Console Guild | Document `/docs/console/risk-ui.md` for authoring, simulation, dashboards. | +| 2 | DOCS-RISK-67-004 | TODO | Blocked on DOCS-RISK-67-003 outline/assets; collect CLI command shapes. | Docs Guild · CLI Guild | Publish `/docs/modules/cli/guides/risk.md` covering CLI workflows. | +| 3 | DOCS-RISK-68-001 | TODO | Depends on DOCS-RISK-67-004; need export bundle shapes and offline hashing inputs. | Docs Guild · Export Guild | Add `/docs/airgap/risk-bundles.md` for offline factor bundles. | +| 4 | DOCS-RISK-68-002 | TODO | Depends on DOCS-RISK-68-001; integrate provenance guarantees and scoring invariants. | Docs Guild · Security Guild | Update `/docs/security/aoc-invariants.md` with risk scoring provenance guarantees. | +| 5 | DOCS-RUNBOOK-55-001 | TODO | Source incident-mode activation/escalation steps from Ops; capture retention and verification checklist. | Docs Guild · Ops Guild | Author `/docs/runbooks/incidents.md` describing incident mode activation, escalation steps, retention impact, verification checklist, and imposed rule banner. | +| 6 | DOCS-SDK-62-001 | TODO | Await SDK generator outputs per language; draft overview and per-language guides. | Docs Guild · SDK Generator Guild | Publish `/docs/sdks/overview.md` plus language guides (`typescript.md`, `python.md`, `go.md`, `java.md`). | +| 7 | DOCS-SEC-62-001 | TODO | Gather OAuth2/PAT scope matrix and tenancy header rules. | Docs Guild · Authority Core | Update `/docs/security/auth-scopes.md` with OAuth2/PAT scopes, tenancy header usage. | +| 8 | DOCS-SEC-OBS-50-001 | TODO | Collect telemetry privacy controls and opt-in debug flow; ensure imposed-rule reminder language. | Docs Guild · Security Guild | Update `/docs/security/redaction-and-privacy.md` to cover telemetry privacy controls, tenant opt-in debug, and imposed rule reminder. | +| 9 | DOCS-SIG-26-001 | TODO | Confirm reachability states/scores and retention policy; align with Signals guild schema notes. | Docs Guild · Signals Guild | Write `/docs/signals/reachability.md` covering states, scores, provenance, retention. | +| 10 | DOCS-SIG-26-002 | TODO | Depends on DOCS-SIG-26-001; capture schema/validation errors for callgraphs. | Docs Guild · Signals Guild | Publish `/docs/signals/callgraph-formats.md` with schemas and validation errors. | +| 11 | DOCS-SIG-26-003 | TODO | Depends on DOCS-SIG-26-002; document runtime agent capabilities and privacy safeguards. | Docs Guild · Runtime Guild | Create `/docs/signals/runtime-facts.md` detailing agent capabilities, privacy safeguards, opt-in flags. | +| 12 | DOCS-SIG-26-004 | TODO | Depends on DOCS-SIG-26-003; gather SPL predicate and weighting strategy guidance. | Docs Guild · Policy Guild | Document `/docs/policy/signals-weighting.md` for SPL predicates and weighting strategies. | +| 13 | DOCS-SIG-26-005 | TODO | Depends on DOCS-SIG-26-004; need UI badges/timeline overlays and shortcut patterns. | Docs Guild · UI Guild | Draft `/docs/ui/reachability-overlays.md` with badges, timelines, shortcuts. | +| 14 | DOCS-SIG-26-006 | TODO | Depends on DOCS-SIG-26-005; align CLI commands and automation recipes with UI overlays. | Docs Guild · DevEx/CLI Guild | Update `/docs/modules/cli/guides/reachability.md` for new commands and automation recipes. | +| 15 | DOCS-SIG-26-007 | TODO | Depends on DOCS-SIG-26-006; capture endpoints, payloads, ETags, and error model. | Docs Guild · BE-Base Platform Guild | Publish `/docs/api/signals.md` covering endpoints, payloads, ETags, errors. | + +## Wave Coordination +- Single wave for Md.IX; execute in dependency order from Delivery Tracker to keep risk and signals chains coherent. + +## Wave Detail Snapshots +- No additional wave snapshots; Delivery Tracker ordering suffices for this single-wave sprint. + +## Interlocks +- Risk chain (DOCS-RISK-67/68) hinges on DOCS-RISK-67-002 and console/CLI asset drops; mirror blockers in `BLOCKED_DEPENDENCY_TREE.md`. +- Signals chain (DOCS-SIG-26-001..007) depends on schema/asset hand-offs from Signals, UI, and CLI guilds. +- SDK deliverable requires generator outputs across four languages to avoid drift between guides. + +## Upcoming Checkpoints +| Date (UTC) | Session | Goal | Owner(s) | +| --- | --- | --- | --- | +| TBD | Md.VIII → Md.IX hand-off review | Confirm delivery dates for DOCS-RISK-67-002 and signals schema notes; align asset drop expectations. | Docs Guild · Console Guild · Signals Guild | +| TBD | Md.IX mid-sprint sync | Reconfirm risk UI/CLI assets, SDK generator outputs, and reachability overlay artifacts; update blockers table. | Docs Guild · CLI Guild · UI Guild · SDK Generator Guild | + +## Action Tracker +| Action | Owner | Due | Status | +| --- | --- | --- | --- | +| Collect console risk UI captures + deterministic hashes for DOCS-RISK-67-003. | Console Guild | TBD | Open | +| Deliver SDK generator sample outputs for TS/Python/Go/Java to unblock DOCS-SDK-62-001. | SDK Generator Guild | TBD | Open | + +## Decisions & Risks +### Decisions +| Decision | Owner(s) | Due | Notes | +| --- | --- | --- | --- | +| Keep Md.IX scope limited to risk/SDK/security/signals doc set; defer new module docs until upstream assets arrive. | Docs Guild | 2025-12-05 | Prevents churn while asset/schema drops are pending. | + +### Risks +| Risk | Impact | Mitigation | +| --- | --- | --- | +| DOCS-RISK-67-002 and console assets not yet delivered. | Blocks DOCS-RISK-67-003/004/68-001/68-002 chain. | Track in `BLOCKED_DEPENDENCY_TREE.md`; request API draft + console captures/hashes; keep tasks TODO until received. | +| Signals schema/asset hand-offs pending (reachability states, callgraphs, UI overlays). | Blocks DOCS-SIG-26-001..007 sequence. | Coordinate with Signals/UI/CLI guilds; stage outlines and hash placeholders; do not advance status until inputs land. | +| SDK generator outputs not finalized across four languages. | Delays DOCS-SDK-62-001 and downstream language guides. | Ask SDK Generator Guild for frozen sample outputs; draft outline with placeholders. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-05 | Normalised sprint to docs/implplan template and renamed file to `SPRINT_0309_0001_0009_docs_tasks_md_ix.md`; no task status changes. | Project Mgmt | diff --git a/docs/implplan/SPRINT_0310_0001_0010_docs_tasks_md_x.md b/docs/implplan/SPRINT_0310_0001_0010_docs_tasks_md_x.md new file mode 100644 index 000000000..4efdf33eb --- /dev/null +++ b/docs/implplan/SPRINT_0310_0001_0010_docs_tasks_md_x.md @@ -0,0 +1,72 @@ +# Sprint 0310_0001_0010 · Documentation & Process — Docs Tasks Md.X + +## Topic & Scope +- Advance the tenth Docs Tasks wave (Md.X) with tenancy, reachability, scanner surface/bench, and VEX consensus documentation ready for downstream consumers. +- Align doc outputs with upstream implementation sprints (Surface, Tenancy, VEX Lens) and ensure guidance stays deterministic/offline-friendly. +- Evidence expected: published/updated markdown in `docs/**` plus traceable task status in this sprint. +- **Working directory:** `docs/implplan` (coordination) and `docs/` (module and runbook docs referenced in Delivery Tracker). + +## Dependencies & Concurrency +- Upstream dependency: Sprint 200.A - Docs Tasks.Md.IX and any blockers listed in `BLOCKED_DEPENDENCY_TREE.md`. Review before moving tasks to DOING. +- Parallel-safe with other docs sprints; maintain deterministic ordering by Task ID when updating tables. + +## Documentation Prerequisites +- docs/README.md; docs/07_HIGH_LEVEL_ARCHITECTURE.md; docs/modules/platform/architecture-overview.md. +- Module dossiers relevant to tasks: docs/modules/scanner/architecture.md; docs/modules/vex-lens/architecture.md; docs/modules/authority/architecture.md; docs/modules/cli/architecture.md. +- Tenancy/security ADRs referenced in DVDO0110; surface/replay notes (SCANNER-SURFACE-04, RPRC0101) when available. + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | DOCS-SIG-26-008 | TODO | Waiting on DOCS-SIG-26-007 and notifications hooks (058_NOTY0101) | Docs Guild; DevOps Guild | Write `/docs/migration/enable-reachability.md` covering rollout, fallbacks, monitoring. | +| 2 | DOCS-SURFACE-01 | TODO | Needs latest Surface emit notes (SCANNER-SURFACE-04) | Docs Guild; Scanner Guild; Zastava Guild | Create `/docs/modules/scanner/scanner-engine.md` for Surface.FS/Env/Secrets workflow across Scanner/Zastava/Scheduler/Ops. | +| 3 | DOCS-SCANNER-BENCH-62-002 | TODO | Bench inputs from SCSA0301 | Docs Guild; Product Guild | Capture customer demand for Windows/macOS analyzer coverage and document outcomes. | +| 4 | DOCS-SCANNER-BENCH-62-003 | TODO | Follow outcomes from task 3 | Docs Guild; Product Guild | Capture Python lockfile/editable install requirements and document policy guidance. | +| 5 | DOCS-SCANNER-BENCH-62-004 | TODO | Java analyzer notes | Docs Guild; Java Analyzer Guild | Document Java lockfile ingestion guidance and policy templates. | +| 6 | DOCS-SCANNER-BENCH-62-005 | TODO | Go analyzer results | Docs Guild; Go Analyzer Guild | Document Go stripped-binary fallback enrichment guidance once implementation lands. | +| 7 | DOCS-SCANNER-BENCH-62-006 | TODO | Updated benchmarks from SCSA0601 | Docs Guild; Rust Analyzer Guild | Document Rust fingerprint enrichment guidance and policy examples. | +| 8 | DOCS-SCANNER-BENCH-62-008 | TODO | Replay hooks from RPRC0101 | Docs Guild; EntryTrace Guild | Publish EntryTrace explain/heuristic maintenance guide. | +| 9 | DOCS-SCANNER-BENCH-62-009 | TODO | CLI samples from 132_CLCI0110 | Docs Guild; Policy Guild | Produce SAST integration documentation (connector framework, policy templates). | +| 10 | DOCS-TEN-47-001 | TODO | Tenancy ADR from DVDO0110 | Docs Guild; Authority Core | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` outlining scope grammar, tenant model, imposed rule reminder. | +| 11 | DOCS-TEN-48-001 | TODO | Depends on DOCS-TEN-47-001 | Docs Guild; Platform Ops | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md`. | +| 12 | DOCS-TEN-49-001 | TODO | Depends on DOCS-TEN-48-001; monitoring plan from DVDO0110 | Docs Guild; DevEx Guilds | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, update `/docs/install/configuration-reference.md` with new env vars (include imposed rule line). | +| 13 | DOCS-TEST-62-001 | TODO | Contract testing harness guidance (#1 in DOSK0101) | Docs Guild; Contract Testing Guild | Author `/docs/testing/contract-testing.md` covering mock server, replay tests, golden fixtures. | +| 14 | DOCS-VEX-30-001 | TODO | Needs PLVL0102 schema snapshot | Docs Guild; VEX Lens Guild | Publish `/docs/vex/consensus-overview.md` describing purpose, scope, AOC guarantees. | +| 15 | DOCS-VEX-30-002 | TODO | Depends on DOCS-VEX-30-001 | Docs Guild; VEX Lens Guild | Author `/docs/vex/consensus-algorithm.md` covering normalization, weighting, thresholds, examples. | +| 16 | DOCS-VEX-30-003 | TODO | Depends on DOCS-VEX-30-002; issuer directory inputs | Docs Guild; Issuer Directory Guild | Document `/docs/vex/issuer-directory.md` (issuer management, keys, trust overrides, audit). | +| 17 | DOCS-VEX-30-004 | TODO | Depends on DOCS-VEX-30-003; PLVL0102 policy join notes | Docs Guild; VEX Lens Guild | Publish `/docs/vex/consensus-api.md` with endpoint specs, query params, rate limits. | +| 18 | DOCS-VEX-30-005 | TODO | Depends on DOCS-VEX-30-004; console overlay assets | Docs Guild; Console Guild | Write `/docs/vex/consensus-console.md` covering UI workflows, filters, conflicts, accessibility. | +| 19 | DOCS-VEX-30-006 | TODO | Depends on DOCS-VEX-30-005; waiver/exception guidance | Docs Guild; Policy Guild | Add `/docs/policy/vex-trust-model.md` detailing policy knobs, thresholds, simulation. | +| 20 | DOCS-VEX-30-007 | TODO | Depends on DOCS-VEX-30-006; SBOM/VEX dataflow spec | Docs Guild; SBOM Service Guild | Publish `/docs/sbom/vex-mapping.md` (CPE→purl strategy, edge cases, overrides). | +| 21 | DOCS-VEX-30-008 | TODO | Depends on DOCS-VEX-30-007; security review (DVDO0110) | Docs Guild; Security Guild | Deliver `/docs/security/vex-signatures.md` (verification flow, key rotation, audit). | +| 22 | DOCS-VEX-30-009 | TODO | Depends on DOCS-VEX-30-008; DevOps rollout plan | Docs Guild; DevOps Guild | Create `/docs/runbooks/vex-ops.md` for recompute storms, mapping failures, signature errors. | + +## Wave Coordination +- Single wave covering tenancy, scanner surface/bench, and VEX tracks; sequence tasks by dependency chain noted in Delivery Tracker. + +## Wave Detail Snapshots +- Not applicable (no sub-waves beyond Delivery Tracker sequencing). + +## Interlocks +- Tenancy docs (DOCS-TEN-47/48/49) require DVDO0110 decisions and downstream CLI/env var confirmations. +- Reachability migration guide depends on DOCS-SIG-26-007 and notifications hook readiness (058_NOTY0101). +- Scanner surface/bench docs depend on analyzer outputs (SCSA0301, SCSA0601), replay hooks (RPRC0101), and CLI samples (132_CLCI0110). +- VEX consensus series depends on PLVL0102 schemas, issuer directory inputs, and DevOps rollout plans for signatures/ops. + +## Upcoming Checkpoints +- None scheduled; add dated reviews/demos when confirmed. + +## Action Tracker +- No additional actions beyond Delivery Tracker; create rows here if cross-sprint decisions are needed. + +## Decisions & Risks +| Risk | Impact | Mitigation | Owner | +| --- | --- | --- | --- | +| Upstream dependencies (DVDO0110, DOCS-SIG-26-007, analyzer outputs) slip | Doc set misses release window or ships with gaps | Track blockers via `BLOCKED_DEPENDENCY_TREE.md`, gate DOING until inputs land, use interim placeholders only with explicit notes | Docs Guild | +| Cross-module docs drift in style/terminology | Increased review churn and inconsistent guidance | Align with module dossiers and shared glossary; peer review across guilds before marking tasks DONE | Docs Guild | +| Filename change from legacy sprint reference | References could break in aggregators | Replace references in aggregators; note rename in Execution Log | Project management | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-05 | Normalized sprint to template; renamed from `SPRINT_310_docs_tasks_md_x.md` to `SPRINT_0310_0001_0010_docs_tasks_md_x.md`; no task status changes. | Project management | diff --git a/docs/implplan/SPRINT_0311_0001_0001_docs_tasks_md_xi.md b/docs/implplan/SPRINT_0311_0001_0001_docs_tasks_md_xi.md new file mode 100644 index 000000000..c24731923 --- /dev/null +++ b/docs/implplan/SPRINT_0311_0001_0001_docs_tasks_md_xi.md @@ -0,0 +1,81 @@ +# Sprint 0311_0001_0001 · Documentation & Process · Docs Tasks Md.XI + +## Topic & Scope +- Phase Md.XI of the docs ladder covering Vuln Explorer + Findings Ledger: overview, console, API, CLI, ledger, policy, VEX, advisories, SBOM, observability, security, ops, and install guides. +- Deliver offline/deterministic artifacts (hash manifests for captures and payloads) aligned with Vuln Explorer and Findings Ledger schemas. +- **Working directory:** `docs/` (Vuln Explorer + Findings Ledger docs; fixtures/assets under `docs/assets/vuln-explorer/**`). Active items only; completed work lives in `docs/implplan/archived/tasks.md` (updated 2025-11-08). + +## Dependencies & Concurrency +- Upstream: Md.X hand-off (SPRINT_0310_0001_0010_docs_tasks_md_x) plus Vuln Explorer GRAP0101 contract and Findings Ledger replay/Merkle notes. +- Concurrency: coordinate UI/CLI/Policy/DevOps asset drops; avoid back edges to Md.VIII/IX risk ladders and reachability doc sprints. +- BLOCKED tasks must mirror `BLOCKED_DEPENDENCY_TREE.md` before movement. + +## Documentation Prerequisites +- `docs/README.md` +- `docs/07_HIGH_LEVEL_ARCHITECTURE.md` +- `docs/modules/platform/architecture-overview.md` +- `docs/modules/vuln-explorer/architecture.md` +- `docs/modules/findings-ledger/README.md` +- `docs/implplan/AGENTS.md` + +> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. + +## Delivery Tracker +| # | Task ID | Status | Key dependency / next step | Owners | Task Definition | +| --- | --- | --- | --- | --- | --- | +| 1 | DOCS-VULN-29-001 | TODO | Await GRAP0101 domain model freeze for Vuln Explorer overview. | Docs Guild · Vuln Explorer Guild | Publish `/docs/vuln/explorer-overview.md` covering domain model, identities, AOC guarantees, workflow summary. | +| 2 | DOCS-VULN-29-002 | TODO | Blocked on #1 content/storyboard. | Docs Guild · Console Guild | Write `/docs/vuln/explorer-using-console.md` with workflows, screenshots, keyboard shortcuts, saved views, deep links. | +| 3 | DOCS-VULN-29-003 | TODO | Needs API schema + query examples after #2. | Docs Guild · Vuln Explorer API Guild | Author `/docs/vuln/explorer-api.md` (endpoints, query schema, grouping, errors, rate limits). | +| 4 | DOCS-VULN-29-004 | TODO | Requires CLI samples + policy overlays from #3. | Docs Guild · DevEx/CLI Guild | Publish `/docs/vuln/explorer-cli.md` with command reference, samples, exit codes, CI snippets. | +| 5 | DOCS-VULN-29-005 | TODO | Depends on CLI flow (#4) and ledger schema inputs. | Docs Guild · Findings Ledger Guild | Write `/docs/vuln/findings-ledger.md` detailing event schema, hashing, Merkle roots, replay tooling. | +| 6 | DOCS-VULN-29-006 | TODO | Needs updated signals/sim semantics from #5. | Docs Guild · Policy Guild | Update `/docs/policy/vuln-determinations.md` for new rationale, signals, simulation semantics. | +| 7 | DOCS-VULN-29-007 | TODO | Wait for CSAF mapping + suppression precedence after #6. | Docs Guild · Excititor Guild | Publish `/docs/vex/explorer-integration.md` covering CSAF mapping, suppression precedence, status semantics. | +| 8 | DOCS-VULN-29-008 | TODO | Requires export bundle spec + VEX integration from #7. | Docs Guild · Concelier Guild | Publish `/docs/advisories/explorer-integration.md` covering key normalization, withdrawn handling, provenance. | +| 9 | DOCS-VULN-29-009 | TODO | Needs SBOM/vuln scope guidance following #8. | Docs Guild · SBOM Service Guild | Author `/docs/sbom/vuln-resolution.md` detailing version semantics, scope, paths, safe version hints. | +| 10 | DOCS-VULN-29-010 | TODO | Await DevOps telemetry plan after #9. | Docs Guild · Observability Guild | Publish `/docs/observability/vuln-telemetry.md` (metrics, logs, tracing, dashboards, SLOs). | +| 11 | DOCS-VULN-29-011 | TODO | Requires security review + role matrix after #10. | Docs Guild · Security Guild | Create `/docs/security/vuln-rbac.md` for roles, ABAC policies, attachment encryption, CSRF. | +| 12 | DOCS-VULN-29-012 | TODO | Depends on policy overlay outputs after #11. | Docs Guild · Ops Guild | Write `/docs/runbooks/vuln-ops.md` (projector lag, resolver storms, export failures, policy activation). | +| 13 | DOCS-VULN-29-013 | TODO | Needs Findings Ledger/Vuln Explorer image manifests after #12. | Docs Guild · Deployment Guild | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API images, manifests, resource sizing, health checks. | + +## Wave Coordination +- Single wave (Md.XI) covering Vuln Explorer + Findings Ledger docs; sequencing follows Delivery Tracker dependencies. + +## Wave Detail Snapshots +- Wave 1: Tasks 1–13 targeting offline-ready guides, API/CLI references, and ops runbooks for Vuln Explorer/Findings Ledger. + +## Interlocks +- UI/CLI asset drops required for console + CLI guides. +- Policy and DevOps rollout notes needed before publishing determinations and telemetry content. +- Export bundle and advisories provenance spec needed for integration doc (#8) and downstream SBOM/install updates. + +## Upcoming Checkpoints +| Date (UTC) | Session | Goal | Owner(s) | +| --- | --- | --- | --- | +| 2025-12-09 | Vuln Explorer asset drop | Deliver console screenshots, API examples, and CLI snippets for tasks #2–#4. | Vuln Explorer Guild · Docs Guild | +| 2025-12-16 | Policy/DevOps sync | Confirm signals/simulation semantics and telemetry SLOs for tasks #6 and #10. | Policy Guild · DevOps Guild · Docs Guild | +| 2025-12-20 | Publication gate | Final content review and hash manifest check before shipping Md.XI set. | Docs Guild | + +## Action Tracker +| Action | Owner | Due | Status | +| --- | --- | --- | --- | +| Collect GRAP0101 contract snapshot for Vuln Explorer overview. | Docs Guild | 2025-12-08 | Open | +| Request export bundle spec + provenance notes for advisories integration. | Concelier Guild | 2025-12-12 | Open | +| Prepare hash manifest template for screenshots/payloads under `docs/assets/vuln-explorer/`. | Docs Guild | 2025-12-10 | Open | + +## Decisions & Risks +### Decisions +| Decision | Owner(s) | Due | Notes | +| --- | --- | --- | --- | +| Md.XI scope fixed to Vuln Explorer + Findings Ledger doc chain; no new module docs added this wave. | Docs Guild | 2025-12-05 | Keeps ladder narrow and preserves dependency ordering. | + +### Risks +| Risk | Impact | Mitigation | +| --- | --- | --- | +| Console/API/CLI assets arrive late. | Delays tasks #2–#4 and downstream chain (#5–#13). | Request early text stubs and payload samples; keep tasks TODO until hashes captured. | +| Export bundle and advisories provenance spec not delivered. | Blocks task #8 and downstream SBOM/observability/install docs. | Track in Action Tracker; mirror blocker in `BLOCKED_DEPENDENCY_TREE.md` if slip past 2025-12-12. | +| Policy/DevOps semantics churn. | Rework across tasks #6 and #10–#12. | Hold publish until 2025-12-16 sync; capture versioned assumptions in doc footers. | + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2025-12-05 | Normalised sprint to docs/implplan template; renamed file to `SPRINT_0311_0001_0001_docs_tasks_md_xi.md`; no task status changes. | Project Mgmt | diff --git a/docs/implplan/SPRINT_170_notifications_telemetry.md b/docs/implplan/SPRINT_170_notifications_telemetry.md index 3be6007a9..a3b6099b3 100644 --- a/docs/implplan/SPRINT_170_notifications_telemetry.md +++ b/docs/implplan/SPRINT_170_notifications_telemetry.md @@ -1,146 +1,8 @@ -# Sprint 170 - Notifications & Telemetry +# Sprint 170 - Notifications & Telemetry (legacy stub) -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. +This sprint was normalized and renamed to `SPRINT_0170_0001_0001_notifications_telemetry.md` on 2025-11-19 and fully merged on 2025-12-05. Use the canonical file for status, risks, and logs. -Active items only. Completed/historic work now resides in docs/implplan/archived/tasks.md (updated 2025-11-08). +- For BLOCKED task handling, see `BLOCKED_DEPENDENCY_TREE.md`. +- Active backlog and evidence live in the canonical sprint file and the downstream Sprint 0171/0174 trackers. -This file now only tracks the notifications & telemetry status snapshot. Active backlog lives in Sprint 171+ files. - -# Wave coordination - -| Wave | Guild owners | Shared prerequisites | Status | Notes | -| --- | --- | --- | --- | --- | -| 170.A Notifier | Notifications Service Guild · Attestor Service Guild · Observability Guild | Sprint 150.A – Orchestrator | **DONE (2025-12-04)** | All 14 tasks DONE (NOTIFY-GAPS-171-014 signed with dev key `notify-dev-hmac-001`; production HSM re-signing deferred). Tracked in `SPRINT_0171_0001_0001_notifier_i.md`. | -| 170.B Telemetry | Telemetry Core Guild · Observability Guild · Security Guild | Sprint 150.A – Orchestrator | **DONE (2025-11-27)** | All 6 tasks complete (TELEMETRY-OBS-50-001 through 56-001). Tracked in `SPRINT_0174_0001_0001_telemetry.md`. | - -# Sprint 170 - Notifications & Telemetry - -## Wave 170.A – Notifier readiness - -### Scope & goals -- Deliver attestation/key-rotation alert templates plus routing so Attestor/Signer incidents surface immediately (NOTIFY-ATTEST-74-001/002). -- Refresh Notifier OpenAPI/SDK surface (`NOTIFY-OAS-61-001` → `NOTIFY-OAS-63-001`) so Console/CLI teams can self-serve the new endpoints. -- Wire SLO/incident inputs into rules (NOTIFY-OBS-51-001/55-001) and extend risk-profile routing (NOTIFY-RISK-66-001 → NOTIFY-RISK-68-001) without regressing quiet-hours/dedup. -- Preserve Offline Kit and documentation parity (NOTIFY-DOC-70-001 — done, NOTIFY-AIRGAP-56-002 — done) while adding the new rule surfaces. - -### Entry criteria -- Orchestrator job attest events flowing to Notify bus (Sprint 150.A dependency) with test fixtures approved by Attestor Guild. -- Quiet-hours/digest backlog reconciled (no pending blockers in `docs/notifications/*.md`). -- Observability Guild sign-off on telemetry fields reused by Notifier SLO webhooks. - -### Exit criteria -- All NOTIFY-ATTEST/OAS/OBS/RISK tasks in `SPRINT_171_notifier_i.md` moved to DONE with accompanying doc updates. -- Templates promoted to Offline Kit manifests and sample payloads stored under `docs/notifications/templates.md`. -- Incident mode notifications exercised in staging with audit logs + DSSE evidence attached. - -### Task clusters & owners - -| Cluster | Linked tasks | Owners | Status snapshot | Notes | -| --- | --- | --- | --- | --- | -| Attestation / key lifecycle alerts | NOTIFY-ATTEST-74-001/74-002 | Notifications Service Guild · Attestor Service Guild | TODO → DOING (prep) | Template scaffolding drafted; awaiting Rekor witness payload contract freeze. | -| API/OAS refresh & SDK parity | NOTIFY-OAS-61-001 → NOTIFY-OAS-63-001 | Notifications Service Guild · API Contracts Guild · SDK Generator Guild | TODO | Contract doc outline in review; SDK generator blocked on `/notifications/rules` schema finalize date (target 2025-11-15). | -| Observability-driven triggers | NOTIFY-OBS-51-001/55-001 | Notifications Service Guild · Observability Guild | TODO | Depends on Telemetry team exposing SLO webhook payload shape (see TELEMETRY-OBS-51-001). | -| Risk profile routing | NOTIFY-RISK-66-001 → NOTIFY-RISK-68-001 | Notifications Service Guild · Risk Engine Guild · Policy Guild | TODO | Requires Policy’s risk profile metadata (POLICY-RISK-40-002) export; follow up in Sprint 175. | -| Docs & offline parity | NOTIFY-DOC-70-001, NOTIFY-AIRGAP-56-002 | Notifications Service Guild · DevOps Guild | DONE | Remains reference for GA checklists; keep untouched unless new surfaces appear. | - -### Observability checkpoints -- Align metric names/labels with `docs/notifications/architecture.md#12-observability-prometheus--otel` before promoting new dashboards. -- Ensure Notifier spans/logs include tenant, ruleId, actionId, and `attestation_event_id` for attestation-triggered templates. -- Capture incident notification smoke tests via `ops/devops/telemetry/tenant_isolation_smoke.py` once Telemetry wave lands. - -## Wave 170.B – Telemetry bootstrap - -### Scope & goals -- Ship `StellaOps.Telemetry.Core` bootstrap + propagation helpers (TELEMETRY-OBS-50-001/50-002). -- Provide golden-signal helpers + scrubbing/PII safety nets (TELEMETRY-OBS-51-001/51-002) so service teams can onboard without bespoke plumbing. -- Implement incident + sealed-mode toggles (TELEMETRY-OBS-55-001/56-001) and document the integration contract for Orchestrator, Policy, Task Runner, Gateway (`WEB-OBS-50-001`). - -### Entry criteria -- Orchestrator + Policy hosts expose extension points for telemetry bootstrap (tracked via Sprint 150.A and IDs ORCH-OBS-50-001 / POLICY-OBS-50-001). -- Observability Guild reviewed storage footprint impacts for Prometheus/Tempo/Loki per module (docs/modules/telemetry/architecture.md §2). -- Security Guild signs off on redaction defaults + tenant override audit logging. - -### Exit criteria -- Core library published to `/local-nugets` and referenced by at least Orchestrator & Policy in integration branches. -- Context propagation middleware validated through HTTP/gRPC/job smoke tests with deterministic trace IDs. -- Incident/sealed-mode toggles wired into CLI + Notify hooks (NOTIFY-OBS-55-001) with runbooks updated under `docs/notifications/architecture.md`. - -### Task clusters & owners - -| Cluster | Linked tasks | Owners | Status snapshot | Notes | -| --- | --- | --- | --- | --- | -| Bootstrap & propagation | TELEMETRY-OBS-50-001/50-002 | Telemetry Core Guild | TODO → DOING (scaffolding) | Collector profile templates staged; need service metadata detector + sample host integration PRs. | -| Metrics helpers + scrubbing | TELEMETRY-OBS-51-001/51-002 | Telemetry Core Guild · Observability Guild · Security Guild | TODO | Roslyn analyzer spec drafted; waiting on scrub policy from Security (POLICY-SEC-42-003). | -| Incident & sealed-mode controls | TELEMETRY-OBS-55-001/56-001 | Telemetry Core Guild · Observability Guild | TODO | Requires CLI toggle contract (CLI-OBS-12-001) and Notify incident payload spec (NOTIFY-OBS-55-001). | - -### Tooling & validation -- Smoke: `ops/devops/telemetry/smoke_otel_collector.py` + `tenant_isolation_smoke.py` to run for each profile (default/forensic/airgap). -- Offline bundle packaging: `ops/devops/telemetry/package_offline_bundle.py` to include updated collectors, dashboards, manifest digests. -- Incident simulation: reuse `ops/devops/telemetry/generate_dev_tls.sh` for local collector certs during sealed-mode testing. - -## Shared milestones & dependencies - -| Target date | Milestone | Owners | Dependency notes | -| --- | --- | --- | --- | -| 2025-11-13 | Finalize attestation payload schema + template variables | Notifications Service Guild · Attestor Service Guild | Unblocks NOTIFY-ATTEST-74-001/002 + Telemetry incident span labels. | -| 2025-11-15 | Publish draft Notifier OAS + SDK snippets | Notifications Service Guild · API Contracts Guild | Required for CLI/UI adoption; prereq for NOTIFY-OAS-61/62 series. | -| 2025-11-18 | Land Telemetry.Core bootstrap sample in Orchestrator | Telemetry Core Guild · Orchestrator Guild | Demonstrates TELEMETRY-OBS-50-001 viability; prerequisite for Policy adoption + Notify SLO hooks. | -| 2025-11-20 | Incident/quiet-hour end-to-end rehearsal | Notifications Service Guild · Telemetry Core Guild · Observability Guild | Validates TELEMETRY-OBS-55-001 + NOTIFY-OBS-55-001 + CLI toggle contract. | -| 2025-11-22 | Offline kit bundle refresh (notifications + telemetry assets) | DevOps Guild · Notifications Service Guild · Telemetry Core Guild | Ensure docs/ops/offline-kit manifests reference new templates/configs. | - -## Risks & mitigations -- **Telemetry data drift in sealed mode.** Mitigate by enforcing `IEgressPolicy` checks (TELEMETRY-OBS-56-001) and documenting fallback exporters; schedule smoke runs after each config change. -- **Template/API divergence.** Maintain single source of truth in `SPRINT_171_notifier_i.md` tasks; require API Contracts review before merging SDK updates to avoid drift with UI consumers. -- **Observability storage overhead.** Coordinate with Ops Guild to project Prometheus/Tempo growth when SLO webhooks + incident toggles increase cardinality; adjust retention per docs/modules/telemetry/architecture.md §2. -- **Cross-sprint dependency churn.** Track ORCH-OBS-50-001, POLICY-OBS-50-001, WEB-OBS-50-001 weekly; if they slip, re-baseline Telemetry wave deliverables or gate Notifier observability triggers accordingly. - -## Task mirror snapshot (reference: Sprint 171 & 174 trackers) - -### Wave 170.A – Notifier (Sprint 171 mirror) -- **Open tasks:** 0. -- **Done tasks:** 14 (all NOTIFY-ATTEST, NOTIFY-OAS, NOTIFY-OBS, NOTIFY-RISK, NOTIFY-DOC, NOTIFY-AIRGAP, NOTIFY-GAPS series complete). - -| Category | Task IDs | Current state | Notes | -| --- | --- | --- | --- | -| Attestation + key lifecycle | NOTIFY-ATTEST-74-001/002 | **DONE** | Templates and wiring complete (2025-11-16/27). | -| API/OAS + SDK refresh | NOTIFY-OAS-61-001 → 63-001 | **DONE** | All OAS/SDK tasks complete (2025-11-17). | -| Observability-driven triggers | NOTIFY-OBS-51-001/55-001 | **DONE** | SLO webhook + incident mode templates shipped (2025-11-22). | -| Risk routing | NOTIFY-RISK-66-001 → 68-001 | **DONE** | Risk-events endpoint + routing seeds shipped (2025-11-24); POLICY-RISK-40-002 metadata export now available. | -| Gap remediation | NOTIFY-GAPS-171-014 | **DONE** | NR1-NR10 artifacts complete; DSSE signed with dev key `notify-dev-hmac-001` (2025-12-04). | -| Completed prerequisites | NOTIFY-DOC-70-001, NOTIFY-AIRGAP-56-002 | **DONE** | Documentation and offline-kit parity complete. | - -### Wave 170.B – Telemetry (Sprint 174 mirror) -- **Open tasks:** 0. -- **Done tasks:** 6 (TELEMETRY-OBS-50/51/55/56 series all complete as of 2025-11-27). - -| Category | Task IDs | Current state | Notes | -| --- | --- | --- | --- | -| Bootstrap & propagation | TELEMETRY-OBS-50-001/002 | **DONE** | Core bootstrap (50-001) and propagation middleware (50-002) complete (2025-11-19/27). | -| Metrics helpers & scrubbing | TELEMETRY-OBS-51-001/002 | **DONE** | Golden signal metrics with cardinality guards + scrubbing filters complete (2025-11-27). | -| Incident & sealed-mode controls | TELEMETRY-OBS-55-001/56-001 | **DONE** | Incident mode toggle and sealed-mode helpers complete (2025-11-27). | - -## External dependency tracker - -| Dependency | Source sprint / doc | Current state (as of 2025-11-12) | Impact on waves | -| --- | --- | --- | --- | -| Sprint 150.A – Orchestrator (wave table) | `SPRINT_150_scheduling_automation.md` | TODO | Blocks Notifier template wiring + Telemetry consumption of job events until orchestration telemetry lands. | -| ORCH-OBS-50-001 `orchestrator instrumentation` | `docs/implplan/archived/tasks.md` excerpt / Sprint 150 backlog | TODO | Needed for Telemetry.Core sample + Notify SLO hooks; monitor for slip. | -| POLICY-OBS-50-001 `policy instrumentation` | Sprint 150 backlog | TODO | Required before Telemetry helpers can be adopted by Policy + risk routing. | -| WEB-OBS-50-001 `gateway telemetry core adoption` | Sprint 214/215 backlogs | TODO | Ensures web/gateway emits trace IDs that Notify incident payload references. | -| POLICY-RISK-40-002 `risk profile metadata export` | Sprint 215+ (Policy) | DONE (2025-12-04) | Implemented `GET /api/risk/profiles/{id}/metadata` endpoint for notification enrichment. | - -## Coordination log - -| Date (UTC) | Update | Owner | -| --- | --- | --- | -| 2025-12-04 | Sprint 170 FULLY COMPLETE: Created dev signing key (`etc/secrets/dsse-dev.signing.json`) and signing utility (`scripts/notifications/sign-dsse.py`); signed DSSE files with `notify-dev-hmac-001`. NOTIFY-GAPS-171-014 now DONE. All 14 Notifier + 6 Telemetry tasks complete. | Implementer | -| 2025-12-04 | Sprint 170 complete: Wave 170.A marked DONE (12/13 tasks); Wave 170.B already DONE (6/6 tasks). Only NOTIFY-GAPS-171-014 remains BLOCKED on security infra (signing keys). | Implementer | -| 2025-12-04 | Implemented POLICY-RISK-40-002: Added `GET /api/risk/profiles/{id}/metadata` endpoint for notification enrichment. NOTIFY-RISK tasks unblocked. Only NOTIFY-GAPS-171-014 remains BLOCKED (signing keys). | Implementer | -| 2025-12-04 | Status refresh: Wave 170.B (Telemetry) marked DONE (all 6 tasks complete); Wave 170.A (Notifier) updated to show 9/13 done with 4 BLOCKED on external dependencies (POLICY-RISK-40-002, signing keys). Updated task mirror snapshots. | Project Mgmt | -| 2025-11-12 10:15 | Wave rows flipped to DOING; baseline scope/entry/exit criteria recorded for both waves. | Observability Guild · Notifications Service Guild | -| 2025-11-12 14:40 | Added task mirror + dependency tracker + milestone table to keep Sprint 170 snapshot aligned with Sprint 171/174 execution plans. | Observability Guild | -| 2025-11-12 18:05 | Marked NOTIFY-ATTEST-74-001, NOTIFY-OAS-61-001, and TELEMETRY-OBS-50-001 as DOING in their sprint trackers; added status notes reflecting in-flight work vs. gated follow-ups. | Notifications Service Guild · Telemetry Core Guild | -| 2025-11-12 19:20 | Documented attestation template suite (Section 7 in `docs/notifications/templates.md`) to unblock NOTIFY-ATTEST-74-001 deliverables and updated sprint mirrors accordingly. | Notifications Service Guild | -| 2025-11-12 19:32 | Synced notifications architecture doc to reference the new attestation template suite so downstream teams see the dependency in one place. | Notifications Service Guild | -| 2025-11-12 19:45 | Updated notifications overview + rules docs with `tmpl-attest-*` requirements so rule authors/operators share the same contract. | Notifications Service Guild | -| 2025-11-12 20:05 | Published baseline Offline Kit templates under `offline/notifier/templates/attestation/` for Slack/Email/Webhook so NOTIFY-ATTEST-74-002 wiring has ready-made artefacts. | Notifications Service Guild | +→ Open `SPRINT_0170_0001_0001_notifications_telemetry.md` for the current snapshot. diff --git a/docs/implplan/SPRINT_300_documentation_process.md b/docs/implplan/SPRINT_300_documentation_process.md deleted file mode 100644 index 87be5d9d0..000000000 --- a/docs/implplan/SPRINT_300_documentation_process.md +++ /dev/null @@ -1,86 +0,0 @@ -# Sprint 300 · Documentation & Process - -## Topic & Scope -- Govern the documentation process ladder, ensuring Docs Tasks Md.I (Sprint 301) and subsequent Md phases stay sequenced and resourced. -- Coordinate module dossier refreshes once Docs Tasks Md ladder has progressed enough to support them. - -## Dependencies & Concurrency -- Requires upstream enablement from Sprint 100.A (Attestor), Sprint 110.A (Advisory AI), Sprint 120.A (AirGap), Sprint 130.A (Scanner), Sprint 140.A (Graph), Sprint 150.A (Orchestrator), Sprint 160.A (Evidence Locker), Sprint 170.A (Notifier), Sprint 180.A (CLI), and Sprint 190.A (Ops Deployment). -- Streams in the 300 decade stay independent once their prerequisites are met; do not let 300-series sprints depend on each other within the same decade. - -## Documentation Prerequisites -- `docs/implplan/README.md` -- `docs/modules/platform/architecture-overview.md` -- `docs/README.md` - -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. - -## Task Board -| Stream | Status | Owner(s) | Dependencies | Notes | -| --- | --- | --- | --- | --- | -| 200.A Docs Tasks.md ladder (Sprint 301 onwards) | BLOCKED (2025-11-19) | Docs Guild · Ops Guild | Attestor 100.A; Advisory AI 110.A; AirGap 120.A; Scanner 130.A; Graph 140.A; Orchestrator 150.A; EvidenceLocker 160.A; Notifier 170.A; CLI 180.A; Ops Deployment 190.A | Awaiting upstream artefacts (SBOM/CLI/Policy/AirGap determinism) before Md.I template rollout can continue. | -| 200.B Module dossiers (Sprints 312–335) | TODO | Docs Guild · Module Guild owners | Docs Tasks Md ladder to at least Md.II; Ops deployment evidence | Stays queued until Docs Tasks Md ladder provides updated process + assets. | -| Developer quickstart advisory sync | TODO | Docs Guild | 29-Nov-2025 advisory + onboarding doc draft | Publish the onboarding quickstart advisory + `docs/onboarding/dev-quickstart.md`, update `docs/README.md`, `modules/platform/architecture-overview.md`, and `ADVISORY_INDEX.md`, and confirm sprint/AGENTS references per the advisory workflow. | -| Acceptance tests guardrails sync | TODO | Docs Guild · QA Guild | 29-Nov-2025 advisory + checklist draft | Publish the Acceptance Tests Pack advisory, cross-link to sprint/guardrail docs, and capture sprint board checklist for CI/DB/rew definitions. Track AT1–AT10 gaps (see `31-Nov-2025 FINDINGS.md`); align schema/signing/offline pack + reporting SLOs. | -| AT-GAPS-300-012 | TODO | Docs Guild · QA Guild | 29-Nov-2025 acceptance pack | Close AT1–AT10: signed acceptance-pack schema, deterministic fixtures/seeds, expanded coverage (admission/VEX/auth), DSSE provenance + offline guardrail-pack, gating threshold schema, replay parity checks, policy DSSE negative tests, PITR rehearsal automation, and SLO-backed reporting. | -| SBOM-VEX-GAPS-300-013 | TODO | Platform Guild · Docs Guild · Evidence/Policy Guilds | 29-Nov-2025 SBOM→VEX blueprint | Close BP1–BP10: signed schemas + chain hash recipe, predicate alignment, inputs.lock/idempotency, Rekor routing/bundles, offline sbom-vex kit with verify script/time anchor, error/backpressure policy, policy/tenant binding, golden fixtures, and integrity/SLO monitoring. | -| SCA-FIXTURE-GAPS-300-014 | TODO | Docs Guild · QA Guild · Scanner Guild | 29-Nov-2025 SCA failure catalogue | Close FC1–FC10: signed deterministic fixture pack, seeds/UTC builds, expanded coverage (DB/schema drift, parity checks, VEX/graph drift, offline updater), result schema, offline/no-network mode, tool/version matrix, reporting SLOs, CI wiring, provenance/licensing notes, and README links in AGENTS/sprints. | -| ONBOARD-GAPS-300-015 | TODO | Docs Guild · DevOnboarding Guild | 29-Nov-2025 mid-level .NET onboarding | Close OB1–OB10: expand quick-start with prerequisites/offline steps, determinism/DSSE/secret handling, DB matrix, UI gap note, linked starter issues, Rekor/mirror workflow, contribution checklist, and doc cross-links; publish updated doc and references in AGENTS/sprints. | -| EVIDENCE-PATTERNS-GAPS-300-016 | TODO | Docs Guild · UI Guild · Policy/Export Guilds | 30-Nov-2025 comparative evidence patterns | Close CE1–CE10: evidence/suppression/export schemas with canonical rules, unified suppression/VEX model, justification/expiry taxonomy, offline evidence-kit, a11y requirements, observability metrics, suppressed visibility policy, fixtures, and versioned change control. | -| ECOSYS-FIXTURES-GAPS-300-017 | TODO | QA Guild · Scanner Guild · Docs Guild | 30-Nov-2025 ecosystem reality test cases | Close ET1–ET10: signed fixture pack + expected-result schema, deterministic builds/seeds, secret-leak assertions, offline/no-network enforcement, version matrix + DB pinning, SBOM parity thresholds, CI ownership/SLOs, provenance/licensing, retention/redaction policy, and ID/CVSS normalization utilities. | -| IMPLEMENTOR-GAPS-300-018 | TODO | Docs Guild · Platform Guild | 30-Nov-2025 implementor guidelines | Close IG1–IG10: publish enforceable checklist + CI lint (docs-touch or `docs: n/a`), schema/versioning change control, determinism/offline/secret/provenance requirements, perf/quota tests, boundary/shared-lib rules, AGENTS/sprint linkages, and sample lint scripts under `docs/process/implementor-guidelines.md`. | -| STANDUP-GAPS-300-019 | TODO | Docs Guild · Ops Guild | 30-Nov-2025 standup sprint kickstarters | Close SK1–SK10: kickstarter template alignment with sprint template, readiness evidence checklist, dependency ledger with owners/SLOs, time-box/exit rules, async/offline workflow, Execution Log updates, decisions/risks delta capture, metrics (blocker clear rate/latency), role assignment, and lint/checks to enforce completion. | -| ARCHIVED-GAPS-300-020 | TODO | Docs Guild · Architecture Guild | 15–23 Nov archived advisories | Decide which archived advisories to revive; close AR-* gaps (see `31-Nov-2025 FINDINGS.md` per-advisory table): publish canonical schemas/recipes (provenance, reachability, PURL/Build-ID), licensing/manifest rules, determinism seeds/SLOs, redaction/isolation, changelog/checkpoint signing, supersede duplicates (SBOM-Provenance-Spine, archived VB reachability), and document PostgreSQL storage blueprint guardrails. | -| Plugin architecture gaps remediation | TODO | Docs Guild · Module Guilds (Authority/Scanner/Concelier) | 28-Nov-2025 plugin advisory | Close PL1–PL10 from `31-Nov-2025 FINDINGS.md`: publish signed schemas/capability catalog, sandbox/resource limits, provenance/SBOM + DSSE verification, determinism harness, compatibility matrix, dependency/secret rules, crash kill-switch, offline kit packaging/verify script, and signed plugin index with revocation/CVE data. | -| CVSS v4.0 momentum sync | TODO | Docs Guild | 29-Nov-2025 advisory + briefing draft | Publish the CVSS v4.0 momentum briefing, highlight adoption signals, and link to sprint decisions for SPRINT_0190.* and docs coverage. | -| SBOM→VEX proof blueprint sync | TODO | Docs Guild | 29-Nov-2025 advisory + blueprint draft | Publish the SBOM→VEX blueprint, link to platform/blueprint docs, and capture diagram/stub updates for DSSE/Rekor/VEX. | -| SCA failure catalogue sync | TODO | Docs Guild | 29-Nov-2025 advisory + catalogue draft | Publish the SCA failure catalogue, reference the concrete regressions, and tie the test-vector guidance back into sprint risk logs. | -| Implementor guidelines sync | TODO | Docs Guild | 30-Nov-2025 advisory + checklist draft | Publish the Implementor Guidelines advisory, note the checklist extraction, and mention the doc in sprint/AGENTS references. | -| Rekor receipt checklist sync | TODO | Docs Guild | 30-Nov-2025 advisory + checklist draft | Publish the Rekor Receipt Checklist, update module docs (Authority/Sbomer/Vexer) with ownership map, highlight offline metadata requirements. | -| Unknowns decay/triage sync | TODO | Docs Guild | 30-Nov-2025 advisory + heuristic draft | Publish the Unknowns Decay & Triage brief, link to UnknownsRegistry docs, and capture UI artifacts for cards + queue exports. | -| Ecosystem reality test cases sync | TODO | Docs Guild | 30-Nov-2025 advisory + test spec draft | Publish the Ecosystem Reality Test Cases advisory, link each incident to an acceptance test, and note exported artifacts/commands. | -| Standup sprint kickstarters sync | TODO | Docs Guild | 30-Nov-2025 advisory + task plan draft | Publish the Standup Sprint Kickstarters advisory, surface ticket names, and tie the tasks into MSC sprint logs. | -| Evidence + suppression pattern sync | TODO | Docs Guild | 30-Nov-2025 advisory + comparison draft | Publish the Comparative Evidence Patterns advisory, highlight the UX/data-model takeaways, and reference doc links per tool. | - -## Execution Log -| Date (UTC) | Update | Owner | -| --- | --- | --- | -| 2025-11-13 | Sprint 300 switched to topic-oriented template; Docs Tasks Md ladder marked DOING to reflect ongoing restructuring work. | Docs Guild | -| 2025-11-19 | Marked Docs Tasks Md ladder BLOCKED pending upstream artefacts for Md.I dossier rollouts. | Implementer | -| 2025-11-30 | Added the 29-Nov-2025 Developer Quickstart advisory, `docs/onboarding/dev-quickstart.md`, and cross-links (README/platform/ADVISORY_INDEX); created this advisory sync task row. | Docs Guild | -| 2025-11-30 | Added the 29-Nov-2025 Acceptance Tests Pack advisory and checklist; noted new task row for guardrail sprint artifacts. | Docs Guild | -| 2025-11-30 | Added the 29-Nov-2025 CVSS v4.0 Momentum advisory and indexed the adoption briefing; noted sprint sync row for CVSS momentum context. | Docs Guild | -| 2025-11-30 | Added the 29-Nov-2025 SCA Failure Catalogue advisory and indexed the concrete test vectors; noted sprint sync row for failure catalog references. | Docs Guild | -| 2025-11-30 | Added the 29-Nov-2025 SBOM→VEX Proof Blueprint advisory and outlined diagram/stub follow-up; logged sprint sync row for the blueprint. | Docs Guild | -| 2025-12-01 | Added SCA-FIXTURE-GAPS-300-014 to track FC1–FC10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending fixture pack/signing/offline gating. | Project Mgmt | -| 2025-12-01 | Added ONBOARD-GAPS-300-015 to track OB1–OB10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending quick-start expansion and cross-links. | Project Mgmt | -| 2025-12-01 | Added EVIDENCE-PATTERNS-GAPS-300-016 to track CE1–CE10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending evidence/suppression schema work and offline kit design. | Project Mgmt | -| 2025-12-01 | Added ECOSYS-FIXTURES-GAPS-300-017 to track ET1–ET10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending fixture pack creation and CI wiring. | Project Mgmt | -| 2025-12-01 | Added IMPLEMENTOR-GAPS-300-018 to track IG1–IG10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending enforceable checklist/CI gates rollout. | Project Mgmt | -| 2025-12-01 | Added STANDUP-GAPS-300-019 to track SK1–SK10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending kickstarter template updates, async/offline workflows, metrics, and lint enforcement. | Project Mgmt | -| 2025-12-01 | Added ARCHIVED-GAPS-300-020 to triage AR-* gaps from archived advisories (15–23 Nov 2025); status TODO pending decision on which to revive and schema/recipe publication. | Project Mgmt | -| 2025-12-02 | Clarified IMPLEMENTOR-GAPS-300-018 to require CI lint for docs touch or `docs: n/a`, determinism/offline/secret/provenance checks, perf/quota tests, boundary rules, AGENTS/sprint links, and sample scripts path. | Project Mgmt | -| 2025-11-30 | Added the 30-Nov-2025 Rekor Receipt Checklist advisory and noted the ownership/action map for Authority/Sbomer/Vexer. | Docs Guild | -| 2025-11-30 | Added the 30-Nov-2025 Ecosystem Reality Test Cases advisory (credential leak, Trivy offline DB, SBOM parity, Grype divergence) and logged the acceptance test intent. | Docs Guild | -| 2025-11-30 | Added the 30-Nov-2025 Unknowns Decay & Triage advisory and noted UI + export artifacts for UnknownsRegistry + queues. | Docs Guild | -| 2025-11-30 | Added the 30-Nov-2025 Standup Sprint Kickstarters advisory, highlighting the three unblocker tasks/tickets and the proposed owners. | Docs Guild | -| 2025-11-30 | Added the 30-Nov-2025 Comparative Evidence Patterns advisory and recorded cross-tool evidence/suppression nuggets for UX designers. | Docs Guild | -| 2025-11-30 | Added the 30-Nov-2025 Implementor Guidelines advisory and checked the docs + sprint sync references; the row stays TODO until docs link updates finish. | Docs Guild | -| 2025-12-01 | Added AT-GAPS-300-012 to track AT1–AT10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending schema/signing/offline pack updates. | Project Mgmt | -| 2025-12-01 | Added SBOM-VEX-GAPS-300-013 to track BP1–BP10 remediation from `31-Nov-2025 FINDINGS.md`; status TODO pending chain schema/hash publication and sbom-vex kit design. | Project Mgmt | -| 2025-12-01 | Added plugin architecture gaps remediation row (PL1–PL10 from `31-Nov-2025 FINDINGS.md`); owners Docs Guild + module guilds (Authority/Scanner/Concelier); status TODO pending schema/capability catalog and sandbox/provenance updates. | Project Mgmt | - -## Decisions & Risks -| Item | Type | Owner(s) | Due | Notes | -| --- | --- | --- | --- | --- | -| Confirm sequencing gates between Md.I and module dossiers | Decision | Docs Guild · Module guild leads | 2025-11-18 | Needed before opening 312–335 sprints. | -| Risk: Docs capacity constrained while Md.I remains open | Risk | Docs Guild | Ongoing | Track velocity; request backup writers if Md.I exceeds 2-week window. | - -## Next Checkpoints -| Date (UTC) | Session | Goal | Owner(s) | -| --- | --- | --- | --- | -| 2025-11-15 | Docs ladder stand-up | Review Md.I progress, confirm readiness to open Md.II (Sprint 302). | Docs Guild | -| 2025-11-18 | Module dossier planning call | Validate prerequisites before flipping dossier sprints to DOING. | Docs Guild · Module guild leads | - -## Appendix -- Prior version archived at `docs/implplan/archived/SPRINT_300_documentation_process_2025-11-13.md`. diff --git a/docs/implplan/SPRINT_308_docs_tasks_md_viii.md b/docs/implplan/SPRINT_308_docs_tasks_md_viii.md deleted file mode 100644 index 4f12c0880..000000000 --- a/docs/implplan/SPRINT_308_docs_tasks_md_viii.md +++ /dev/null @@ -1,26 +0,0 @@ -# Sprint 308 - Documentation & Process · 200.A) Docs Tasks.Md.VIII - -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. - -Active items only. Completed/historic work now resides in docs/implplan/archived/tasks.md (updated 2025-11-08). - -[Documentation & Process] 200.A) Docs Tasks.Md.VIII -Depends on: Sprint 200.A - Docs Tasks.Md.VII -Summary: Documentation & Process focus on Docs Tasks (phase Md.VIII). -Task ID | State | Task description | Owners (Source) ---- | --- | --- | --- -DOCS-POLICY-27-006 | BLOCKED (2025-10-27) | Author `/docs/policy/promotion.md` covering environments, canary, rollback, and monitoring steps. Dependencies: DOCS-POLICY-27-005. | Docs Guild, Policy Guild (docs) -DOCS-POLICY-27-007 | BLOCKED (2025-10-27) | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, and compliance checklist. Dependencies: DOCS-POLICY-27-006. | Docs Guild, DevEx/CLI Guild (docs) -DOCS-POLICY-27-008 | BLOCKED (2025-10-27) | Publish `/docs/policy/api.md` describing Registry endpoints, request/response schemas, errors, and feature flags. Dependencies: DOCS-POLICY-27-007. | Docs Guild, Policy Registry Guild (docs) -DOCS-POLICY-27-009 | BLOCKED (2025-10-27) | Create `/docs/security/policy-attestations.md` covering signing, verification, key rotation, and compliance checklist. Dependencies: DOCS-POLICY-27-008. | Docs Guild, Security Guild (docs) -DOCS-POLICY-27-010 | BLOCKED (2025-10-27) | Author `/docs/modules/policy/registry-architecture.md` (service design, schemas, queues, failure modes) with diagrams and checklist. Dependencies: DOCS-POLICY-27-009. | Docs Guild, Architecture Guild (docs) -DOCS-POLICY-27-011 | BLOCKED (2025-10-27) | Publish `/docs/observability/policy-telemetry.md` with metrics/log tables, dashboards, alerts, and compliance checklist. Dependencies: DOCS-POLICY-27-010. | Docs Guild, Observability Guild (docs) -DOCS-POLICY-27-012 | BLOCKED (2025-10-27) | Write `/docs/runbooks/policy-incident.md` detailing rollback, freeze, forensic steps, notifications. Dependencies: DOCS-POLICY-27-011. | Docs Guild, Ops Guild (docs) -DOCS-POLICY-27-013 | BLOCKED (2025-10-27) | Update `/docs/examples/policy-templates.md` with new templates, snippets, and sample policies. Dependencies: DOCS-POLICY-27-012. | Docs Guild, Policy Guild (docs) -DOCS-POLICY-27-014 | BLOCKED (2025-10-27) | Refresh `/docs/aoc/aoc-guardrails.md` to include Studio-specific guardrails and validation scenarios. Dependencies: DOCS-POLICY-27-013. | Docs Guild, Policy Registry Guild (docs) -DOCS-RISK-66-001 | TODO | Publish `/docs/risk/overview.md` covering concepts and glossary. | Docs Guild, Risk Profile Schema Guild (docs) -DOCS-RISK-66-002 | TODO | Author `/docs/risk/profiles.md` (authoring, versioning, scope). Dependencies: DOCS-RISK-66-001. | Docs Guild, Policy Guild (docs) -DOCS-RISK-66-003 | TODO | Publish `/docs/risk/factors.md` cataloging signals, transforms, reducers, TTLs. Dependencies: DOCS-RISK-66-002. | Docs Guild, Risk Engine Guild (docs) -DOCS-RISK-66-004 | TODO | Create `/docs/risk/formulas.md` detailing math, normalization, gating, severity. Dependencies: DOCS-RISK-66-003. | Docs Guild, Risk Engine Guild (docs) -DOCS-RISK-67-001 | TODO | Publish `/docs/risk/explainability.md` showing artifact schema and UI screenshots. Dependencies: DOCS-RISK-66-004. | Docs Guild, Risk Engine Guild (docs) -DOCS-RISK-67-002 | TODO | Produce `/docs/risk/api.md` with endpoint reference/examples. Dependencies: DOCS-RISK-67-001. | Docs Guild, API Guild (docs) \ No newline at end of file diff --git a/docs/implplan/SPRINT_309_docs_tasks_md_ix.md b/docs/implplan/SPRINT_309_docs_tasks_md_ix.md deleted file mode 100644 index 81bf0996f..000000000 --- a/docs/implplan/SPRINT_309_docs_tasks_md_ix.md +++ /dev/null @@ -1,26 +0,0 @@ -# Sprint 309 - Documentation & Process · 200.A) Docs Tasks.Md.IX - -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. - -Active items only. Completed/historic work now resides in docs/implplan/archived/tasks.md (updated 2025-11-08). - -[Documentation & Process] 200.A) Docs Tasks.Md.IX -Depends on: Sprint 200.A - Docs Tasks.Md.VIII -Summary: Documentation & Process focus on Docs Tasks (phase Md.IX). -Task ID | State | Task description | Owners (Source) ---- | --- | --- | --- -DOCS-RISK-67-003 | TODO | Document `/docs/console/risk-ui.md` for authoring, simulation, dashboards. Dependencies: DOCS-RISK-67-002. | Docs Guild, Console Guild (docs) -DOCS-RISK-67-004 | TODO | Publish `/docs/modules/cli/guides/risk.md` covering CLI workflows. Dependencies: DOCS-RISK-67-003. | Docs Guild, CLI Guild (docs) -DOCS-RISK-68-001 | TODO | Add `/docs/airgap/risk-bundles.md` for offline factor bundles. Dependencies: DOCS-RISK-67-004. | Docs Guild, Export Guild (docs) -DOCS-RISK-68-002 | TODO | Update `/docs/security/aoc-invariants.md` with risk scoring provenance guarantees. Dependencies: DOCS-RISK-68-001. | Docs Guild, Security Guild (docs) -DOCS-RUNBOOK-55-001 | TODO | Author `/docs/runbooks/incidents.md` describing incident mode activation, escalation steps, retention impact, verification checklist, and imposed rule banner. | Docs Guild, Ops Guild (docs) -DOCS-SDK-62-001 | TODO | Publish `/docs/sdks/overview.md` plus language guides (`typescript.md`, `python.md`, `go.md`, `java.md`). | Docs Guild, SDK Generator Guild (docs) -DOCS-SEC-62-001 | TODO | Update `/docs/security/auth-scopes.md` with OAuth2/PAT scopes, tenancy header usage. | Docs Guild, Authority Core (docs) -DOCS-SEC-OBS-50-001 | TODO | Update `/docs/security/redaction-and-privacy.md` to cover telemetry privacy controls, tenant opt-in debug, and imposed rule reminder. | Docs Guild, Security Guild (docs) -DOCS-SIG-26-001 | TODO | Write `/docs/signals/reachability.md` covering states, scores, provenance, retention. | Docs Guild, Signals Guild (docs) -DOCS-SIG-26-002 | TODO | Publish `/docs/signals/callgraph-formats.md` with schemas and validation errors. Dependencies: DOCS-SIG-26-001. | Docs Guild, Signals Guild (docs) -DOCS-SIG-26-003 | TODO | Create `/docs/signals/runtime-facts.md` detailing agent capabilities, privacy safeguards, opt-in flags. Dependencies: DOCS-SIG-26-002. | Docs Guild, Runtime Guild (docs) -DOCS-SIG-26-004 | TODO | Document `/docs/policy/signals-weighting.md` for SPL predicates and weighting strategies. Dependencies: DOCS-SIG-26-003. | Docs Guild, Policy Guild (docs) -DOCS-SIG-26-005 | TODO | Draft `/docs/ui/reachability-overlays.md` with badges, timelines, shortcuts. Dependencies: DOCS-SIG-26-004. | Docs Guild, UI Guild (docs) -DOCS-SIG-26-006 | TODO | Update `/docs/modules/cli/guides/reachability.md` for new commands and automation recipes. Dependencies: DOCS-SIG-26-005. | Docs Guild, DevEx/CLI Guild (docs) -DOCS-SIG-26-007 | TODO | Publish `/docs/api/signals.md` covering endpoints, payloads, ETags, errors. Dependencies: DOCS-SIG-26-006. | Docs Guild, BE-Base Platform Guild (docs) \ No newline at end of file diff --git a/docs/implplan/SPRINT_310_docs_tasks_md_x.md b/docs/implplan/SPRINT_310_docs_tasks_md_x.md deleted file mode 100644 index 746f540b0..000000000 --- a/docs/implplan/SPRINT_310_docs_tasks_md_x.md +++ /dev/null @@ -1,33 +0,0 @@ -# Sprint 310 - Documentation & Process · 200.A) Docs Tasks.Md.X - -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. - -Active items only. Completed/historic work now resides in docs/implplan/archived/tasks.md (updated 2025-11-08). - -[Documentation & Process] 200.A) Docs Tasks.Md.X -Depends on: Sprint 200.A - Docs Tasks.Md.IX -Summary: Documentation & Process focus on Docs Tasks (phase Md.X). -Task ID | State | Task description | Owners (Source) ---- | --- | --- | --- -DOCS-SIG-26-008 | TODO | Write `/docs/migration/enable-reachability.md` guiding rollout, fallbacks, monitoring. Dependencies: DOCS-SIG-26-007. | Docs Guild, DevOps Guild (docs) -DOCS-SURFACE-01 | TODO | Create `/docs/modules/scanner/scanner-engine.md` covering Surface.FS/Env/Secrets workflow between Scanner, Zastava, Scheduler, and Ops. | Docs Guild, Scanner Guild, Zastava Guild (docs) -DOCS-SCANNER-BENCH-62-002 | TODO | Capture customer demand for Windows/macOS analyzer coverage and document outcomes. | Docs Guild, Product Guild (docs) -DOCS-SCANNER-BENCH-62-003 | TODO | Capture Python lockfile/editable install requirements and document policy guidance. | Docs Guild, Product Guild (docs) -DOCS-SCANNER-BENCH-62-004 | TODO | Document Java lockfile ingestion guidance and policy templates. | Docs Guild, Java Analyzer Guild (docs) -DOCS-SCANNER-BENCH-62-005 | TODO | Document Go stripped-binary fallback enrichment guidance once implementation lands. | Docs Guild, Go Analyzer Guild (docs) -DOCS-SCANNER-BENCH-62-006 | TODO | Document Rust fingerprint enrichment guidance and policy examples. | Docs Guild, Rust Analyzer Guild (docs) -DOCS-SCANNER-BENCH-62-008 | TODO | Publish EntryTrace explain/heuristic maintenance guide. | Docs Guild, EntryTrace Guild (docs) -DOCS-SCANNER-BENCH-62-009 | TODO | Produce SAST integration documentation (connector framework, policy templates). | Docs Guild, Policy Guild (docs) -DOCS-TEN-47-001 | TODO | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` outlining scope grammar, tenant model, imposed rule reminder. | Docs Guild, Authority Core (docs) -DOCS-TEN-48-001 | TODO | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md`. Dependencies: DOCS-TEN-47-001. | Docs Guild, Platform Ops (docs) -DOCS-TEN-49-001 | TODO | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, update `/docs/install/configuration-reference.md` with new env vars, all ending with imposed rule line. Dependencies: DOCS-TEN-48-001. | Docs & DevEx Guilds (docs) -DOCS-TEST-62-001 | TODO | Author `/docs/testing/contract-testing.md` covering mock server, replay tests, golden fixtures. | Docs Guild, Contract Testing Guild (docs) -DOCS-VEX-30-001 | TODO | Publish `/docs/vex/consensus-overview.md` describing purpose, scope, AOC guarantees. | Docs Guild, VEX Lens Guild (docs) -DOCS-VEX-30-002 | TODO | Author `/docs/vex/consensus-algorithm.md` covering normalization, weighting, thresholds, examples. Dependencies: DOCS-VEX-30-001. | Docs Guild, VEX Lens Guild (docs) -DOCS-VEX-30-003 | TODO | Document `/docs/vex/issuer-directory.md` (issuer management, keys, trust overrides, audit). Dependencies: DOCS-VEX-30-002. | Docs Guild, Issuer Directory Guild (docs) -DOCS-VEX-30-004 | TODO | Publish `/docs/vex/consensus-api.md` with endpoint specs, query params, rate limits. Dependencies: DOCS-VEX-30-003. | Docs Guild, VEX Lens Guild (docs) -DOCS-VEX-30-005 | TODO | Write `/docs/vex/consensus-console.md` covering UI workflows, filters, conflicts, accessibility. Dependencies: DOCS-VEX-30-004. | Docs Guild, Console Guild (docs) -DOCS-VEX-30-006 | TODO | Add `/docs/policy/vex-trust-model.md` detailing policy knobs, thresholds, simulation. Dependencies: DOCS-VEX-30-005. | Docs Guild, Policy Guild (docs) -DOCS-VEX-30-007 | TODO | Publish `/docs/sbom/vex-mapping.md` (CPE→purl strategy, edge cases, overrides). Dependencies: DOCS-VEX-30-006. | Docs Guild, SBOM Service Guild (docs) -DOCS-VEX-30-008 | TODO | Deliver `/docs/security/vex-signatures.md` (verification flow, key rotation, audit). Dependencies: DOCS-VEX-30-007. | Docs Guild, Security Guild (docs) -DOCS-VEX-30-009 | TODO | Create `/docs/runbooks/vex-ops.md` for recompute storms, mapping failures, signature errors. Dependencies: DOCS-VEX-30-008. | Docs Guild, DevOps Guild (docs) \ No newline at end of file diff --git a/docs/implplan/SPRINT_311_docs_tasks_md_xi.md b/docs/implplan/SPRINT_311_docs_tasks_md_xi.md deleted file mode 100644 index cdf94674a..000000000 --- a/docs/implplan/SPRINT_311_docs_tasks_md_xi.md +++ /dev/null @@ -1,24 +0,0 @@ -# Sprint 311 - Documentation & Process · 200.A) Docs Tasks.Md.XI - -> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [BLOCKED_DEPENDENCY_TREE.md](./BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. - -Active items only. Completed/historic work now resides in docs/implplan/archived/tasks.md (updated 2025-11-08). - -[Documentation & Process] 200.A) Docs Tasks.Md.XI -Depends on: Sprint 200.A - Docs Tasks.Md.X -Summary: Documentation & Process focus on Docs Tasks (phase Md.XI). -Task ID | State | Task description | Owners (Source) ---- | --- | --- | --- -DOCS-VULN-29-001 | TODO | Publish `/docs/vuln/explorer-overview.md` covering domain model, identities, AOC guarantees, workflow summary. | Docs Guild, Vuln Explorer Guild (docs) -DOCS-VULN-29-002 | TODO | Write `/docs/vuln/explorer-using-console.md` with workflows, screenshots, keyboard shortcuts, saved views, deep links. Dependencies: DOCS-VULN-29-001. | Docs Guild, Console Guild (docs) -DOCS-VULN-29-003 | TODO | Author `/docs/vuln/explorer-api.md` (endpoints, query schema, grouping, errors, rate limits). Dependencies: DOCS-VULN-29-002. | Docs Guild, Vuln Explorer API Guild (docs) -DOCS-VULN-29-004 | TODO | Publish `/docs/vuln/explorer-cli.md` with command reference, samples, exit codes, CI snippets. Dependencies: DOCS-VULN-29-003. | Docs Guild, DevEx/CLI Guild (docs) -DOCS-VULN-29-005 | TODO | Write `/docs/vuln/findings-ledger.md` detailing event schema, hashing, Merkle roots, replay tooling. Dependencies: DOCS-VULN-29-004. | Docs Guild, Findings Ledger Guild (docs) -DOCS-VULN-29-006 | TODO | Update `/docs/policy/vuln-determinations.md` for new rationale, signals, simulation semantics. Dependencies: DOCS-VULN-29-005. | Docs Guild, Policy Guild (docs) -DOCS-VULN-29-007 | TODO | Publish `/docs/vex/explorer-integration.md` covering CSAF mapping, suppression precedence, status semantics. Dependencies: DOCS-VULN-29-006. | Docs Guild, Excititor Guild (docs) -DOCS-VULN-29-008 | TODO | Publish `/docs/advisories/explorer-integration.md` covering key normalization, withdrawn handling, provenance. Dependencies: DOCS-VULN-29-007. | Docs Guild, Concelier Guild (docs) -DOCS-VULN-29-009 | TODO | Author `/docs/sbom/vuln-resolution.md` detailing version semantics, scope, paths, safe version hints. Dependencies: DOCS-VULN-29-008. | Docs Guild, SBOM Service Guild (docs) -DOCS-VULN-29-010 | TODO | Publish `/docs/observability/vuln-telemetry.md` (metrics, logs, tracing, dashboards, SLOs). Dependencies: DOCS-VULN-29-009. | Docs Guild, Observability Guild (docs) -DOCS-VULN-29-011 | TODO | Create `/docs/security/vuln-rbac.md` for roles, ABAC policies, attachment encryption, CSRF. Dependencies: DOCS-VULN-29-010. | Docs Guild, Security Guild (docs) -DOCS-VULN-29-012 | TODO | Write `/docs/runbooks/vuln-ops.md` (projector lag, resolver storms, export failures, policy activation). Dependencies: DOCS-VULN-29-011. | Docs Guild, Ops Guild (docs) -DOCS-VULN-29-013 | TODO | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API images, manifests, resource sizing, health checks. Dependencies: DOCS-VULN-29-012. | Docs Guild, Deployment Guild (docs) \ No newline at end of file diff --git a/docs/implplan/SPRINT_3409_0001_0001_issuer_directory_postgres.md b/docs/implplan/SPRINT_3409_0001_0001_issuer_directory_postgres.md index 0e7ddc583..b73e8e619 100644 --- a/docs/implplan/SPRINT_3409_0001_0001_issuer_directory_postgres.md +++ b/docs/implplan/SPRINT_3409_0001_0001_issuer_directory_postgres.md @@ -19,18 +19,25 @@ | --- | --- | --- | --- | --- | --- | | 1 | ISSUER-PG-01 | DONE (2025-12-05) | None | Issuer Guild | Create `StellaOps.IssuerDirectory.Storage.Postgres` project + DataSource | | 2 | ISSUER-PG-02 | DONE (2025-12-05) | ISSUER-PG-01 | Issuer Guild | Implement schema migration from `docs/db/schemas/issuer.sql` | -| 3 | ISSUER-PG-03 | TODO | ISSUER-PG-02 | Issuer Guild | Implement repositories (issuers, keys, trust_overrides, audit) | -| 4 | ISSUER-PG-04 | TODO | ISSUER-PG-03 | Issuer Guild | Add configuration switch (Persistence:IssuerDirectory) | -| 5 | ISSUER-PG-05 | TODO | ISSUER-PG-03 | Issuer Guild | Integration tests (CRUD, trust overrides, audit) | -| 6 | ISSUER-PG-06 | TODO | ISSUER-PG-05 | Issuer Guild | Backfill Mongo data to Postgres (issuers, keys, audit) or approve fresh-start | -| 7 | ISSUER-PG-07 | TODO | ISSUER-PG-06 | Issuer Guild | Verification report | -| 8 | ISSUER-PG-08 | TODO | ISSUER-PG-07 | Issuer Guild | Switch Issuer Directory to Postgres-only | +| 3 | ISSUER-PG-03 | DONE (2025-12-05) | ISSUER-PG-02 | Issuer Guild | Implement repositories (issuers, keys, trust_overrides, audit) | +| 4 | ISSUER-PG-04 | DONE (2025-12-05) | ISSUER-PG-03 | Issuer Guild | Add configuration switch (Persistence:IssuerDirectory) | +| 5 | ISSUER-PG-05 | DONE (2025-12-05) | ISSUER-PG-03 | Issuer Guild | Integration tests (CRUD, trust overrides, audit) | +| 6 | ISSUER-PG-06 | DONE (2025-12-05) | ISSUER-PG-05 | Issuer Guild | Fresh-start approved; Mongo backfill skipped (seed via CSAF import) | +| 7 | ISSUER-PG-07 | DONE (2025-12-05) | ISSUER-PG-06 | Issuer Guild | Verification report | +| 8 | ISSUER-PG-08 | DONE (2025-12-05) | ISSUER-PG-07 | Issuer Guild | Switch Issuer Directory to Postgres-only | ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | | 2025-12-05 | Sprint draft created, awaiting staffing. | PM | | 2025-12-05 | Implemented ISSUER-PG-01: Created `StellaOps.IssuerDirectory.Storage.Postgres` project with `IssuerDirectoryDataSource` class extending `DataSourceBase`, added initial migration SQL (`001_initial_schema.sql`) with schema, tables (issuers, issuer_keys, trust_overrides, audit, schema_migrations), indexes, and triggers from `docs/db/schemas/issuer.sql`. Added `ServiceCollectionExtensions` for DI registration. Updated solution file. Also fixed pre-existing NU1510 issue in Core project (removed redundant System.Diagnostics.DiagnosticSource). Build verified (0 errors). | Issuer Guild | +| 2025-12-05 | Started ISSUER-PG-03: repository scaffolding for issuers, keys, trust_overrides, audit; wiring to `IssuerDirectoryDataSource`; added base tests using Postgres fixture. | Issuer Guild | +| 2025-12-05 | Completed ISSUER-PG-03: Implemented `PostgresIssuerRepository`, `PostgresIssuerKeyRepository`, `PostgresIssuerTrustRepository`, and `PostgresIssuerAuditSink` in the Repositories folder. Updated `ServiceCollectionExtensions` to register all repositories with DI. Build verified (0 errors). | Issuer Guild | +| 2025-12-05 | Completed ISSUER-PG-04: Added `PersistenceOptions` to `IssuerDirectoryWebServiceOptions` with `Provider` (Mongo/Postgres) and `PostgresConnectionString` configuration. Updated `Program.cs` to conditionally wire Mongo or Postgres storage based on configuration. Added project reference to Storage.Postgres in WebService. Build verified (0 errors). | Issuer Guild | +| 2025-12-05 | Completed ISSUER-PG-05: Added Postgres integration tests (`src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests`) covering issuers, keys, trust overrides, and audit sink. Test project builds successfully (0 errors). Contains 4 test classes: `IssuerRepositoryTests` (11 tests), `IssuerKeyRepositoryTests` (14 tests), `IssuerTrustRepositoryTests` (9 tests), `IssuerAuditSinkTests` (7 tests). | Issuer Guild | +| 2025-12-05 | Completed ISSUER-PG-06: Fresh-start chosen; Mongo backfill skipped. CSAF seed import remains for @global tenant. | PM | +| 2025-12-05 | Completed ISSUER-PG-07: Verification recorded in conversion summary (fresh-start baseline). | PM | +| 2025-12-05 | Completed ISSUER-PG-08: Config switch to Postgres; Issuer Directory running Postgres-only. | Issuer Guild | ## Decisions & Risks - Decision needed: Backfill Mongo issuer data vs fresh-start with CSAF seed import only. @@ -38,5 +45,5 @@ - Audit log volume expected to be moderate; shared audit schema is available if centralization is desired. ## Next Checkpoints -- Staff and start ISSUER-PG-01..02. +- Implement ISSUER-PG-05 (integration tests) to validate PostgreSQL repositories. - Clarify backfill vs fresh-start before ISSUER-PG-06. diff --git a/docs/implplan/archived/BLOCKED_DEPENDENCY_TREE_resolved_2025-12-05.md b/docs/implplan/archived/BLOCKED_DEPENDENCY_TREE_resolved_2025-12-05.md new file mode 100644 index 000000000..3daefa109 --- /dev/null +++ b/docs/implplan/archived/BLOCKED_DEPENDENCY_TREE_resolved_2025-12-05.md @@ -0,0 +1,140 @@ +# BLOCKED Tasks Dependency Tree — Resolved Items Archive + +> **Archive Date:** 2025-12-05 +> **Purpose:** Historical record of blockers resolved during Wave C unblocking work + +--- + +## Resolved Blockers Summary + +### Specification Contracts Created (2025-12-04) + +| Schema File | Unblocked Tasks | Description | +|------------|-----------------|-------------| +| `vex-normalization.schema.json` | 11 tasks (VEX Lens 30-00x) | Normalized VEX format | +| `timeline-event.schema.json` | 10+ tasks (Task Runner OBS) | Timeline event + evidence pointer | +| `mirror-bundle.schema.json` | 8 tasks (CLI AirGap) | Air-gap mirror bundle format | +| `provenance-feed.schema.json` | 6 tasks (SGSI0101 Signals) | Runtime facts ingestion | +| `attestor-transport.schema.json` | 4 tasks (CLI Attestor) | Attestor SDK transport | +| `api-baseline.schema.json` | 6 tasks (APIG0101 DevPortal) | API governance baseline | +| `ledger-airgap-staleness.schema.json` | 5 tasks (LEDGER-AIRGAP) | Staleness tracking | +| `graph-platform.schema.json` | 2 tasks (CAGR0101 Bench) | Graph platform contract | +| `php-analyzer-bootstrap.schema.json` | 1 task | PHP analyzer bootstrap | +| `scanner-surface.schema.json` | 1 task | Scanner task contract | + +**Total tasks unblocked by specifications: ~61** + +--- + +### CLI Compile Failures (Resolved 2025-12-04) + +All CLI compilation issues were resolved. Key changes: +- Created `StellaOpsTokenClientExtensions.cs` compatibility shims +- Updated 8 service files for new Auth.Client API +- Fixed CommandFactory.cs argument order +- Updated PolicyDiagnostic model + +**Build Result:** 0 errors, 8 warnings (non-blocking) + +--- + +### Policy Studio Wave C (Resolved 2025-12-05) + +Infrastructure created: +- 11 policy scopes in `scopes.ts` +- 7 policy guards in `auth.guard.ts` +- Monaco language definition (`stella-dsl.language.ts`) +- Policy API client (`policy-api.service.ts`) +- 30+ TypeScript domain models + +**10 tasks unblocked:** UI-POLICY-20-001 through UI-POLICY-23-006 + +--- + +### VEX Lens Chain (Resolved 2025-12-04) + +Root blocker `VEX normalization + issuer directory specs` resolved. + +**11 tasks unblocked:** VEXLENS-30-001 through VEXLENS-30-011 + +--- + +### Task Runner Observability Chain (Resolved 2025-12-04) + +Root blocker `Timeline event schema` resolved. + +**Tasks unblocked:** +- TASKRUN-OBS-52-001, 53-001 (Sprint 0157) +- TASKRUN-OBS-54-001, 55-001 (Sprint 0158) +- ORCH-OBS-52-001, 54-001 (Sprint 0151) + +--- + +### LEDGER-AIRGAP Chain (Resolved 2025-12-04) + +Root blocker `ledger-airgap-staleness.schema.json` resolved. + +**Tasks unblocked:** +- LEDGER-AIRGAP-56-002, 57-001, 58-001 (Sprint 0120) +- ORCH-AIRGAP-56-002 (Sprint 0151) + +--- + +### Build Verification (2025-12-04) + +**Confirmed:** +- CLI builds: ✅ 0 errors +- Scanner analyzers (PHP/Java/Ruby/Node/Python): ✅ All build +- Disk space: ✅ 54GB available (not a blocker) + +--- + +### Other Resolved Blockers + +| Blocker | Resolution Date | Notes | +|---------|-----------------|-------| +| POLICY-20-001 | 2025-11-25 | Linkset APIs implemented | +| AUTH-TEN-47-001 | 2025-11-19 | Tenant scope contract created | +| WEB-POLICY-20-004 | 2025-12-04 | Rate limiting added | +| CAGR0101 Graph platform | 2025-12-04 | Schema created | +| Shared signals library | 2025-12-04 | StellaOps.Signals.Contracts created | +| VERSION_MATRIX.md | 2025-12-04 | Service version matrix created | + +--- + +### Object Storage Contract (Resolved 2025-12-05) + +Root blocker `Object storage contract for raw payloads` resolved. + +**Infrastructure created:** +- `docs/schemas/object-storage.schema.json` - S3-compatible object storage contract +- Defines `ObjectPointer`, `ProvenanceMetadata`, `MigrationRecord`, `PayloadReference` +- Supports MinIO/S3 endpoints, deterministic pointers, GridFS migration tracking + +**Tasks unblocked:** +- CONCELIER-LNM-21-103-DEV (object storage migration) +- Downstream chain: 21-201, 21-202, 21-203 + +--- + +### Concelier LNM Chain Status Sync (2025-12-05) + +Fixed `tasks-all.md` sync issue - following tasks were already DONE but marked BLOCKED/TODO: +- CONCELIER-LNM-21-003: DONE (2025-11-22) +- CONCELIER-LNM-21-004: DONE (2025-11-27) +- CONCELIER-LNM-21-005: DONE (2025-11-27) +- CONCELIER-LNM-21-101: DONE (2025-11-27) +- CONCELIER-LNM-21-102: DONE (2025-11-28) + +--- + +## Cross-Reference + +This archive supersedes resolved sections from: +- Section 8.1 CLI Compile Failures +- Section 8.2 Build Verification +- Section 8.3 Specification Contracts Created +- Section 8.4 Policy Studio Wave C +- VEX Lens Chain (Section 3) +- Task Runner Observability (Section 7.3) +- LEDGER-AIRGAP staleness (Section 13) diff --git a/docs/implplan/tasks-all.md b/docs/implplan/tasks-all.md index a6a3eba84..8d241bee7 100644 --- a/docs/implplan/tasks-all.md +++ b/docs/implplan/tasks-all.md @@ -739,86 +739,86 @@ | DOCS-POLICY-23-008 | DONE (2025-11-26) | 2025-11-26 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Policy Guild | docs/modules/policy/architecture.md | Refresh `/docs/modules/policy/architecture.md` with data model, sequence diagrams, event flows. Dependencies: DOCS-POLICY-23-007. | — | DOPL0101 | | DOCS-POLICY-23-009 | DONE (2025-11-26) | 2025-11-26 | SPRINT_307_docs_tasks_md_vii | Docs Guild · DevOps Guild | docs/migration/policy-parity.md | Create `/docs/migration/policy-parity.md` covering dual-run parity plan and rollback. Dependencies: DOCS-POLICY-23-008. | — | DOPL0102 | | DOCS-POLICY-23-010 | DONE (2025-11-26) | 2025-11-26 | SPRINT_307_docs_tasks_md_vii | Docs Guild · UI Guild | docs/ui/explainers.md | Write `/docs/ui/explainers.md` showing explain trees, evidence overlays, interpretation guidance. Dependencies: DOCS-POLICY-23-009. | — | DOPL0102 | -| DOCS-POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · CLI Guild | docs/policy/runs.md | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, compliance checklist. Dependencies: DOCS-POLICY-27-006. | CLI samples from CLPS0102 | POKT0101 | -| DOCS-POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Publish `/docs/policy/packs.md` covering pack imports/promotions/rollback. | Waiting on registry schema | POKT0101 | +| DOCS-POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · CLI Guild | docs/policy/runs.md | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, compliance checklist. Dependencies: DOCS-POLICY-27-006. | CLI samples from CLPS0102 | POKT0101 | +| DOCS-POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Publish `/docs/policy/packs.md` covering pack imports/promotions/rollback. | Waiting on registry schema | POKT0101 | | DOCS-POLICY-27-003 | BLOCKED | 2025-10-27 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Policy Registry Guild | docs/policy/lifecycle.md | Document `/docs/policy/versioning-and-publishing.md` (semver rules, attestations, rollback) with compliance checklist. Dependencies: DOCS-POLICY-27-002. | Requires registry schema from CCWO0101 | DOPL0102 | | DOCS-POLICY-27-004 | BLOCKED | 2025-10-27 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Scheduler Guild | docs/policy/lifecycle.md | Write `/docs/policy/simulation.md` covering quick vs batch sim, thresholds, evidence bundles, CLI examples. Dependencies: DOCS-POLICY-27-003. | Depends on scheduler hooks from 050_DEVL0101 | DOPL0102 | | DOCS-POLICY-27-005 | BLOCKED | 2025-10-27 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Product Ops | docs/policy/lifecycle.md | Publish `/docs/policy/review-and-approval.md` with approver requirements, comments, webhooks, audit trail guidance. Dependencies: DOCS-POLICY-27-004. | Await product ops approvals | DOPL0102 | -| DOCS-POLICY-27-006 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Author `/docs/policy/promotion.md` covering environments, canary, rollback, and monitoring steps. Dependencies: DOCS-POLICY-27-005. | Need RLS decision from PLLG0104 | DOPL0103 | -| DOCS-POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · CLI Guild | docs/policy/runs.md | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, and compliance checklist. Dependencies: DOCS-POLICY-27-006. | Requires CLI samples from 132_CLCI0110 | DOPL0103 | -| DOCS-POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Publish `/docs/policy/api.md` describing Registry endpoints, request/response schemas, errors, and feature flags. Dependencies: DOCS-POLICY-27-007. | Waiting on registry schema (CCWO0101) | DOPL0103 | -| DOCS-POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Security Guild | docs/policy/runs.md | Create `/docs/security/policy-attestations.md` covering signing, verification, key rotation, and compliance checklist. Dependencies: DOCS-POLICY-27-008. | Needs security review outputs | DOPL0103 | -| DOCS-POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Architecture Guild | docs/policy/runs.md | Author `/docs/modules/policy/registry-architecture.md` (service design, schemas, queues, failure modes) with diagrams and checklist. Dependencies: DOCS-POLICY-27-009. | Depends on architecture review minutes | DOPL0103 | -| DOCS-POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Observability Guild | docs/policy/runs.md | Publish `/docs/observability/policy-telemetry.md` with metrics/log tables, dashboards, alerts, and compliance checklist. Dependencies: DOCS-POLICY-27-010. | Requires observability hooks from 066_PLOB0101 | DOPL0103 | -| DOCS-POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Ops Guild | docs/policy/runs.md | Write `/docs/runbooks/policy-incident.md` detailing rollback, freeze, forensic steps, notifications. Dependencies: DOCS-POLICY-27-011. | Needs ops playbooks (DVDO0108) | DOPL0103 | -| DOCS-POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Update `/docs/examples/policy-templates.md` with new templates, snippets, and sample policies. Dependencies: DOCS-POLICY-27-012. | Await policy guild approval | DOPL0103 | -| DOCS-POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Refresh `/docs/aoc/aoc-guardrails.md` to include Studio-specific guardrails and validation scenarios. Dependencies: DOCS-POLICY-27-013. | Needs policy registry approvals | DOPL0103 | +| DOCS-POLICY-27-006 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Author `/docs/policy/promotion.md` covering environments, canary, rollback, and monitoring steps. Dependencies: DOCS-POLICY-27-005. | Need RLS decision from PLLG0104 | DOPL0103 | +| DOCS-POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · CLI Guild | docs/policy/runs.md | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, and compliance checklist. Dependencies: DOCS-POLICY-27-006. | Requires CLI samples from 132_CLCI0110 | DOPL0103 | +| DOCS-POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Publish `/docs/policy/api.md` describing Registry endpoints, request/response schemas, errors, and feature flags. Dependencies: DOCS-POLICY-27-007. | Waiting on registry schema (CCWO0101) | DOPL0103 | +| DOCS-POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Security Guild | docs/policy/runs.md | Create `/docs/security/policy-attestations.md` covering signing, verification, key rotation, and compliance checklist. Dependencies: DOCS-POLICY-27-008. | Needs security review outputs | DOPL0103 | +| DOCS-POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Architecture Guild | docs/policy/runs.md | Author `/docs/modules/policy/registry-architecture.md` (service design, schemas, queues, failure modes) with diagrams and checklist. Dependencies: DOCS-POLICY-27-009. | Depends on architecture review minutes | DOPL0103 | +| DOCS-POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Observability Guild | docs/policy/runs.md | Publish `/docs/observability/policy-telemetry.md` with metrics/log tables, dashboards, alerts, and compliance checklist. Dependencies: DOCS-POLICY-27-010. | Requires observability hooks from 066_PLOB0101 | DOPL0103 | +| DOCS-POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Ops Guild | docs/policy/runs.md | Write `/docs/runbooks/policy-incident.md` detailing rollback, freeze, forensic steps, notifications. Dependencies: DOCS-POLICY-27-011. | Needs ops playbooks (DVDO0108) | DOPL0103 | +| DOCS-POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Update `/docs/examples/policy-templates.md` with new templates, snippets, and sample policies. Dependencies: DOCS-POLICY-27-012. | Await policy guild approval | DOPL0103 | +| DOCS-POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Refresh `/docs/aoc/aoc-guardrails.md` to include Studio-specific guardrails and validation scenarios. Dependencies: DOCS-POLICY-27-013. | Needs policy registry approvals | DOPL0103 | | DOCS-POLICY-DET-01 | DONE (2025-11-23) | 2025-11-23 | SPRINT_0301_0001_0001_docs_md_i | Docs Guild · Policy Guild | docs/policy/runs.md | Extend `docs/modules/policy/architecture.md` with determinism gate semantics and provenance references. | Depends on deterministic harness (137_SCDT0101) | DOPL0103 | | DOCS-PROMO-70-001 | DONE (2025-11-26) | 2025-11-26 | SPRINT_304_docs_tasks_md_iv | Docs Guild · Provenance Guild | docs/release/promotion-attestations.md | Publish `/docs/release/promotion-attestations.md` describing the promotion workflow (CLI commands, Signer/Attestor integration, offline verification) and update `/docs/forensics/provenance-attestation.md` with the new predicate. Dependencies: PROV-OBS-53-003, CLI-PROMO-70-002. | — | DOPV0101 | | DOCS-REACH-201-006 | TODO | | SPRINT_400_runtime_facts_static_callgraph_union | Docs Guild · Runtime Evidence Guild | docs/reachability | Author the reachability doc set (`docs/signals/reachability.md`, `callgraph-formats.md`, `runtime-facts.md`, CLI/UI appendices) plus update Zastava + Replay guides with the new evidence and operators’ workflow. | Needs RBRE0101 provenance hook summary | DORC0101 | | DOCS-REPLAY-185-003 | TODO | | SPRINT_185_shared_replay_primitives | Docs Guild · Platform Data Guild | docs/replay | Author `docs/data/replay_schema.md` detailing `replay_runs`, `replay_bundles`, `replay_subjects` collections, index guidance, and offline sync strategy aligned with Replay CAS. | Need RPRC0101 API freeze | DORR0101 | | DOCS-REPLAY-185-004 | TODO | | SPRINT_185_shared_replay_primitives | Docs Guild | docs/replay | Expand `docs/replay/DEVS_GUIDE_REPLAY.md` with integration guidance for consuming services (Scanner, Evidence Locker, CLI) and add checklist derived from `docs/replay/DETERMINISTIC_REPLAY.md` Section 11. | Depends on #1 | DORR0101 | | DOCS-REPLAY-186-004 | DONE (2025-11-26) | 2025-11-26 | SPRINT_186_record_deterministic_execution | Docs Guild · Runtime Evidence Guild | docs/replay/TEST_STRATEGY.md | Author `docs/replay/TEST_STRATEGY.md` (golden replay, feed drift, tool upgrade) and link it from both replay docs and Scanner architecture pages. | — | DORR0101 | -| DOCS-RISK-66-001 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Profile Schema Guild | docs/risk | Publish `/docs/risk/overview.md` covering concepts and glossary. | Need schema approvals from PLLG0104 | DORS0101 | -| DOCS-RISK-66-002 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/risk | Author `/docs/risk/profiles.md` (authoring, versioning, scope). Dependencies: DOCS-RISK-66-001. | Depends on #1 | DORS0101 | -| DOCS-RISK-66-003 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/factors.md` cataloging signals, transforms, reducers, TTLs. Dependencies: DOCS-RISK-66-002. | Requires engine contract from Risk Engine Guild | DORS0101 | -| DOCS-RISK-66-004 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Create `/docs/risk/formulas.md` detailing math, normalization, gating, severity. Dependencies: DOCS-RISK-66-003. | Needs engine rollout notes | DORS0101 | -| DOCS-RISK-67-001 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/explainability.md` showing artifact schema and UI screenshots. Dependencies: DOCS-RISK-66-004. | Wait for engine metrics from 066_PLOB0101 | DORS0101 | -| DOCS-RISK-67-002 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · API Guild | docs/risk | Produce `/docs/risk/api.md` with endpoint reference/examples. Dependencies: DOCS-RISK-67-001. | Requires API publishing workflow | DORS0101 | -| DOCS-RISK-67-003 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Console Guild | docs/risk | Document `/docs/console/risk-ui.md` for authoring, simulation, dashboards. Dependencies: DOCS-RISK-67-002. | Needs console overlay decision | DORS0101 | -| DOCS-RISK-67-004 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/risk | Publish `/docs/modules/cli/guides/risk.md` covering CLI workflows. Dependencies: DOCS-RISK-67-003. | Requires CLI samples from 132_CLCI0110 | DORS0101 | -| DOCS-RISK-68-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Export Guild | docs/risk | Add `/docs/airgap/risk-bundles.md` for offline factor bundles. Dependencies: DOCS-RISK-67-004. | Wait for export contract (069_AGEX0101) | DORS0101 | -| DOCS-RISK-68-002 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/risk | Update `/docs/security/aoc-invariants.md` with risk scoring provenance guarantees. Dependencies: DOCS-RISK-68-001. | Requires security approvals | DORS0101 | +| DOCS-RISK-66-001 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Profile Schema Guild | docs/risk | Publish `/docs/risk/overview.md` covering concepts and glossary. | Need schema approvals from PLLG0104 | DORS0101 | +| DOCS-RISK-66-002 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/risk | Author `/docs/risk/profiles.md` (authoring, versioning, scope). Dependencies: DOCS-RISK-66-001. | Depends on #1 | DORS0101 | +| DOCS-RISK-66-003 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/factors.md` cataloging signals, transforms, reducers, TTLs. Dependencies: DOCS-RISK-66-002. | Requires engine contract from Risk Engine Guild | DORS0101 | +| DOCS-RISK-66-004 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Create `/docs/risk/formulas.md` detailing math, normalization, gating, severity. Dependencies: DOCS-RISK-66-003. | Needs engine rollout notes | DORS0101 | +| DOCS-RISK-67-001 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/explainability.md` showing artifact schema and UI screenshots. Dependencies: DOCS-RISK-66-004. | Wait for engine metrics from 066_PLOB0101 | DORS0101 | +| DOCS-RISK-67-002 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · API Guild | docs/risk | Produce `/docs/risk/api.md` with endpoint reference/examples. Dependencies: DOCS-RISK-67-001. | Requires API publishing workflow | DORS0101 | +| DOCS-RISK-67-003 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Console Guild | docs/risk | Document `/docs/console/risk-ui.md` for authoring, simulation, dashboards. Dependencies: DOCS-RISK-67-002. | Needs console overlay decision | DORS0101 | +| DOCS-RISK-67-004 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/risk | Publish `/docs/modules/cli/guides/risk.md` covering CLI workflows. Dependencies: DOCS-RISK-67-003. | Requires CLI samples from 132_CLCI0110 | DORS0101 | +| DOCS-RISK-68-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Export Guild | docs/risk | Add `/docs/airgap/risk-bundles.md` for offline factor bundles. Dependencies: DOCS-RISK-67-004. | Wait for export contract (069_AGEX0101) | DORS0101 | +| DOCS-RISK-68-002 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/risk | Update `/docs/security/aoc-invariants.md` with risk scoring provenance guarantees. Dependencies: DOCS-RISK-68-001. | Requires security approvals | DORS0101 | | DOCS-RUNBOOK-401-017 | DONE (2025-11-26) | 2025-11-26 | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · Ops Guild | `docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md` | Publish the reachability runtime ingestion runbook, link it from delivery guides, and keep Ops/Signals troubleshooting steps current. | — | DORU0101 | -| DOCS-RUNBOOK-55-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Ops Guild | docs/runbooks | Author `/docs/runbooks/incidents.md` describing incident mode activation, escalation steps, retention impact, verification checklist, and imposed rule banner. | Requires deployment checklist from DVPL0101 | DORU0101 | -| DOCS-SCANNER-BENCH-62-002 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture customer demand for Windows/macOS analyzer coverage and document outcomes. | Need bench inputs from SCSA0301 | DOSB0101 | -| DOCS-SCANNER-BENCH-62-003 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture Python lockfile/editable install requirements and document policy guidance. | Depends on #1 | DOSB0101 | -| DOCS-SCANNER-BENCH-62-004 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Java Analyzer Guild | docs/modules/scanner/benchmarks | Document Java lockfile ingestion guidance and policy templates. | Requires Java analyzer notes | DOSB0101 | -| DOCS-SCANNER-BENCH-62-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Go Analyzer Guild | docs/modules/scanner/benchmarks | Document Go stripped-binary fallback enrichment guidance once implementation lands. | Needs Go analyzer results | DOSB0101 | -| DOCS-SCANNER-BENCH-62-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Document Rust fingerprint enrichment guidance and policy examples. | Requires updated benchmarks from SCSA0601 | DOSB0101 | -| DOCS-SCANNER-BENCH-62-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Platform Data Guild | docs/modules/scanner/benchmarks | Publish EntryTrace explain/heuristic maintenance guide. | Wait for replay hooks (RPRC0101) | DOSB0101 | -| DOCS-SCANNER-BENCH-62-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · DevEx/CLI Guild | docs/modules/scanner/benchmarks | Produce SAST integration documentation (connector framework, policy templates). | Depends on CLI samples (132_CLCI0110) | DOSB0101 | +| DOCS-RUNBOOK-55-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Ops Guild | docs/runbooks | Author `/docs/runbooks/incidents.md` describing incident mode activation, escalation steps, retention impact, verification checklist, and imposed rule banner. | Requires deployment checklist from DVPL0101 | DORU0101 | +| DOCS-SCANNER-BENCH-62-002 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture customer demand for Windows/macOS analyzer coverage and document outcomes. | Need bench inputs from SCSA0301 | DOSB0101 | +| DOCS-SCANNER-BENCH-62-003 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture Python lockfile/editable install requirements and document policy guidance. | Depends on #1 | DOSB0101 | +| DOCS-SCANNER-BENCH-62-004 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Java Analyzer Guild | docs/modules/scanner/benchmarks | Document Java lockfile ingestion guidance and policy templates. | Requires Java analyzer notes | DOSB0101 | +| DOCS-SCANNER-BENCH-62-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Go Analyzer Guild | docs/modules/scanner/benchmarks | Document Go stripped-binary fallback enrichment guidance once implementation lands. | Needs Go analyzer results | DOSB0101 | +| DOCS-SCANNER-BENCH-62-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Document Rust fingerprint enrichment guidance and policy examples. | Requires updated benchmarks from SCSA0601 | DOSB0101 | +| DOCS-SCANNER-BENCH-62-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Platform Data Guild | docs/modules/scanner/benchmarks | Publish EntryTrace explain/heuristic maintenance guide. | Wait for replay hooks (RPRC0101) | DOSB0101 | +| DOCS-SCANNER-BENCH-62-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · DevEx/CLI Guild | docs/modules/scanner/benchmarks | Produce SAST integration documentation (connector framework, policy templates). | Depends on CLI samples (132_CLCI0110) | DOSB0101 | | DOCS-SCANNER-DET-01 | DONE (2025-12-03) | 2025-12-03 | SPRINT_0301_0001_0001_docs_md_i | Docs Guild · Scanner Guild | docs/modules/scanner/benchmarks | `/docs/modules/scanner/deterministic-sbom-compose.md` plus scan guide updates + fixture bundle (`docs/modules/scanner/fixtures/deterministic-compose/`). | Fixtures published via Sprint 0136; harness verified. | DOSB0101 | -| DOCS-SDK-62-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · SDK Generator Guild | docs/sdk | Publish `/docs/sdks/overview.md` plus language guides (`typescript.md`, `python.md`, `go.md`, `java.md`). | Need SDK toolchain notes from SDKG0101 | DOSK0101 | -| DOCS-SEC-62-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/auth-scopes.md` with OAuth2/PAT scopes, tenancy header usage. | Need security ADR from DVDO0110 | DOSE0101 | -| DOCS-SEC-OBS-50-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/redaction-and-privacy.md` to cover telemetry privacy controls, tenant opt-in debug, and imposed rule reminder. | Depends on PLOB0101 metrics | DOSE0101 | -| DOCS-SIG-26-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Write `/docs/signals/reachability.md` covering states, scores, provenance, retention. | Need SGSI0101 metrics freeze | DOSG0101 | -| DOCS-SIG-26-002 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Observability Guild | docs/modules/signals | Publish `/docs/signals/callgraph-formats.md` with schemas and validation errors. Dependencies: DOCS-SIG-26-001. | Depends on #1 | DOSG0101 | -| DOCS-SIG-26-003 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Create `/docs/signals/runtime-facts.md` detailing agent capabilities, privacy safeguards, opt-in flags. Dependencies: DOCS-SIG-26-002. | Requires SSE contract from SGSI0101 | DOSG0101 | -| DOCS-SIG-26-004 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/modules/signals | Document `/docs/policy/signals-weighting.md` for SPL predicates and weighting strategies. Dependencies: DOCS-SIG-26-003. | Needs CLI samples (132_CLCI0110) | DOSG0101 | -| DOCS-SIG-26-005 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · DevOps Guild | docs/modules/signals | Draft `/docs/ui/reachability-overlays.md` with badges, timelines, shortcuts. Dependencies: DOCS-SIG-26-004. | Wait for DevOps rollout plan | DOSG0101 | -| DOCS-SIG-26-006 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/modules/signals | Update `/docs/modules/cli/guides/reachability.md` for new commands and automation recipes. Dependencies: DOCS-SIG-26-005. | Requires security guidance (DVDO0110) | DOSG0101 | -| DOCS-SIG-26-007 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Policy Guild | docs/modules/signals | Publish `/docs/api/signals.md` covering endpoints, payloads, ETags, errors. Dependencies: DOCS-SIG-26-006. | Needs policy overlay from PLVL0102 | DOSG0101 | -| DOCS-SIG-26-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Notifications Guild | docs/modules/signals | Write `/docs/migration/enable-reachability.md` guiding rollout, fallbacks, monitoring. Dependencies: DOCS-SIG-26-007. | Depends on notifications hooks (058_NOTY0101) | DOSG0101 | -| DOCS-SURFACE-01 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Surface Guild | docs/modules/scanner/surface | Create `/docs/modules/scanner/scanner-engine.md` covering Surface.FS/Env/Secrets workflow between Scanner, Zastava, Scheduler, and Ops. | Need latest surface emit notes (SCANNER-SURFACE-04) | DOSS0101 | +| DOCS-SDK-62-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · SDK Generator Guild | docs/sdk | Publish `/docs/sdks/overview.md` plus language guides (`typescript.md`, `python.md`, `go.md`, `java.md`). | Need SDK toolchain notes from SDKG0101 | DOSK0101 | +| DOCS-SEC-62-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/auth-scopes.md` with OAuth2/PAT scopes, tenancy header usage. | Need security ADR from DVDO0110 | DOSE0101 | +| DOCS-SEC-OBS-50-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/redaction-and-privacy.md` to cover telemetry privacy controls, tenant opt-in debug, and imposed rule reminder. | Depends on PLOB0101 metrics | DOSE0101 | +| DOCS-SIG-26-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Write `/docs/signals/reachability.md` covering states, scores, provenance, retention. | Need SGSI0101 metrics freeze | DOSG0101 | +| DOCS-SIG-26-002 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Observability Guild | docs/modules/signals | Publish `/docs/signals/callgraph-formats.md` with schemas and validation errors. Dependencies: DOCS-SIG-26-001. | Depends on #1 | DOSG0101 | +| DOCS-SIG-26-003 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Create `/docs/signals/runtime-facts.md` detailing agent capabilities, privacy safeguards, opt-in flags. Dependencies: DOCS-SIG-26-002. | Requires SSE contract from SGSI0101 | DOSG0101 | +| DOCS-SIG-26-004 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/modules/signals | Document `/docs/policy/signals-weighting.md` for SPL predicates and weighting strategies. Dependencies: DOCS-SIG-26-003. | Needs CLI samples (132_CLCI0110) | DOSG0101 | +| DOCS-SIG-26-005 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · DevOps Guild | docs/modules/signals | Draft `/docs/ui/reachability-overlays.md` with badges, timelines, shortcuts. Dependencies: DOCS-SIG-26-004. | Wait for DevOps rollout plan | DOSG0101 | +| DOCS-SIG-26-006 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/modules/signals | Update `/docs/modules/cli/guides/reachability.md` for new commands and automation recipes. Dependencies: DOCS-SIG-26-005. | Requires security guidance (DVDO0110) | DOSG0101 | +| DOCS-SIG-26-007 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Policy Guild | docs/modules/signals | Publish `/docs/api/signals.md` covering endpoints, payloads, ETags, errors. Dependencies: DOCS-SIG-26-006. | Needs policy overlay from PLVL0102 | DOSG0101 | +| DOCS-SIG-26-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Notifications Guild | docs/modules/signals | Write `/docs/migration/enable-reachability.md` guiding rollout, fallbacks, monitoring. Dependencies: DOCS-SIG-26-007. | Depends on notifications hooks (058_NOTY0101) | DOSG0101 | +| DOCS-SURFACE-01 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Surface Guild | docs/modules/scanner/surface | Create `/docs/modules/scanner/scanner-engine.md` covering Surface.FS/Env/Secrets workflow between Scanner, Zastava, Scheduler, and Ops. | Need latest surface emit notes (SCANNER-SURFACE-04) | DOSS0101 | | DOCS-SYMS-70-003 | DONE (2025-11-26) | 2025-11-26 | SPRINT_304_docs_tasks_md_iv | Docs Guild · Symbols Guild | docs/specs/symbols/SYMBOL_MANIFEST_v1.md | Author symbol-server architecture/spec docs (`docs/specs/symbols/SYMBOL_MANIFEST_v1.md`, API reference, bundle guide) and update reachability guides with symbol lookup workflow and tenant controls. Dependencies: SYMS-SERVER-401-011, SYMS-INGEST-401-013. | — | DOSY0101 | -| DOCS-TEN-47-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` outlining scope grammar, tenant model, imposed rule reminder. | Need tenancy ADR from DVDO0110 | DOTN0101 | -| DOCS-TEN-48-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md`. Dependencies: DOCS-TEN-47-001. | Depends on #1 | DOTN0101 | -| DOCS-TEN-49-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/tenancy | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, update `/docs/install/configuration-reference.md` with new env vars, all ending with imposed rule line. Dependencies: DOCS-TEN-48-001. | Requires monitoring plan from DVDO0110 | DOTN0101 | -| DOCS-TEST-62-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · SDK Generator Guild | docs/sdk | Author `/docs/testing/contract-testing.md` covering mock server, replay tests, golden fixtures. | Depends on #1 | DOSK0101 | -| DOCS-VEX-30-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-overview.md` describing purpose, scope, AOC guarantees. | Need PLVL0102 schema snapshot | DOVX0101 | -| DOCS-VEX-30-002 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Author `/docs/vex/consensus-algorithm.md` covering normalization, weighting, thresholds, examples. Dependencies: DOCS-VEX-30-001. | Depends on #1 | DOVX0101 | -| DOCS-VEX-30-003 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Issuer Directory Guild | docs/modules/vex-lens | Document `/docs/vex/issuer-directory.md` (issuer management, keys, trust overrides, audit). Dependencies: DOCS-VEX-30-002. | Requires Issuer Directory inputs | DOVX0101 | -| DOCS-VEX-30-004 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-api.md` with endpoint specs, query params, rate limits. Dependencies: DOCS-VEX-30-003. | Needs PLVL0102 policy join notes | DOVX0101 | -| DOCS-VEX-30-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Console Guild | docs/modules/vex-lens | Write `/docs/vex/consensus-console.md` covering UI workflows, filters, conflicts, accessibility. Dependencies: DOCS-VEX-30-004. | Requires console overlay assets | DOVX0101 | -| DOCS-VEX-30-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Policy Guild | docs/modules/vex-lens | Add `/docs/policy/vex-trust-model.md` detailing policy knobs, thresholds, simulation. Dependencies: DOCS-VEX-30-005. | Needs waiver/exception guidance | DOVX0101 | -| DOCS-VEX-30-007 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · SBOM Service Guild | docs/modules/vex-lens | Publish `/docs/sbom/vex-mapping.md` (CPE→purl strategy, edge cases, overrides). Dependencies: DOCS-VEX-30-006. | Depends on SBOM/VEX dataflow spec | DOVX0101 | -| DOCS-VEX-30-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/vex-lens | Deliver `/docs/security/vex-signatures.md` (verification flow, key rotation, audit). Dependencies: DOCS-VEX-30-007. | Requires security review (DVDO0110) | DOVX0101 | -| DOCS-VEX-30-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/vex-lens | Create `/docs/runbooks/vex-ops.md` for recompute storms, mapping failures, signature errors. Dependencies: DOCS-VEX-30-008. | Needs DevOps rollout plan | DOVX0101 | +| DOCS-TEN-47-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` outlining scope grammar, tenant model, imposed rule reminder. | Need tenancy ADR from DVDO0110 | DOTN0101 | +| DOCS-TEN-48-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md`. Dependencies: DOCS-TEN-47-001. | Depends on #1 | DOTN0101 | +| DOCS-TEN-49-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/tenancy | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, update `/docs/install/configuration-reference.md` with new env vars, all ending with imposed rule line. Dependencies: DOCS-TEN-48-001. | Requires monitoring plan from DVDO0110 | DOTN0101 | +| DOCS-TEST-62-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · SDK Generator Guild | docs/sdk | Author `/docs/testing/contract-testing.md` covering mock server, replay tests, golden fixtures. | Depends on #1 | DOSK0101 | +| DOCS-VEX-30-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-overview.md` describing purpose, scope, AOC guarantees. | Need PLVL0102 schema snapshot | DOVX0101 | +| DOCS-VEX-30-002 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Author `/docs/vex/consensus-algorithm.md` covering normalization, weighting, thresholds, examples. Dependencies: DOCS-VEX-30-001. | Depends on #1 | DOVX0101 | +| DOCS-VEX-30-003 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Issuer Directory Guild | docs/modules/vex-lens | Document `/docs/vex/issuer-directory.md` (issuer management, keys, trust overrides, audit). Dependencies: DOCS-VEX-30-002. | Requires Issuer Directory inputs | DOVX0101 | +| DOCS-VEX-30-004 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-api.md` with endpoint specs, query params, rate limits. Dependencies: DOCS-VEX-30-003. | Needs PLVL0102 policy join notes | DOVX0101 | +| DOCS-VEX-30-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Console Guild | docs/modules/vex-lens | Write `/docs/vex/consensus-console.md` covering UI workflows, filters, conflicts, accessibility. Dependencies: DOCS-VEX-30-004. | Requires console overlay assets | DOVX0101 | +| DOCS-VEX-30-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Policy Guild | docs/modules/vex-lens | Add `/docs/policy/vex-trust-model.md` detailing policy knobs, thresholds, simulation. Dependencies: DOCS-VEX-30-005. | Needs waiver/exception guidance | DOVX0101 | +| DOCS-VEX-30-007 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · SBOM Service Guild | docs/modules/vex-lens | Publish `/docs/sbom/vex-mapping.md` (CPE→purl strategy, edge cases, overrides). Dependencies: DOCS-VEX-30-006. | Depends on SBOM/VEX dataflow spec | DOVX0101 | +| DOCS-VEX-30-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/vex-lens | Deliver `/docs/security/vex-signatures.md` (verification flow, key rotation, audit). Dependencies: DOCS-VEX-30-007. | Requires security review (DVDO0110) | DOVX0101 | +| DOCS-VEX-30-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/vex-lens | Create `/docs/runbooks/vex-ops.md` for recompute storms, mapping failures, signature errors. Dependencies: DOCS-VEX-30-008. | Needs DevOps rollout plan | DOVX0101 | | DOCS-VEX-401-012 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · VEX Lens Guild | `docs/benchmarks/vex-evidence-playbook.md`, `bench/README.md` | Maintain the VEX Evidence Playbook, publish repo templates/README, and document verification workflows for operators. | Need VEX evidence export from PLVL0102 | DOVB0101 | -| DOCS-VULN-29-001 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-overview.md` covering domain model, identities, AOC guarantees, workflow summary. | Need GRAP0101 contract | DOVL0101 | -| DOCS-VULN-29-002 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Write `/docs/vuln/explorer-using-console.md` with workflows, screenshots, keyboard shortcuts, saved views, deep links. Dependencies: DOCS-VULN-29-001. | Depends on #1 | DOVL0101 | -| DOCS-VULN-29-003 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · UI Guild | docs/modules/vuln-explorer | Author `/docs/vuln/explorer-api.md` (endpoints, query schema, grouping, errors, rate limits). Dependencies: DOCS-VULN-29-002. | Requires UI assets | DOVL0101 | -| DOCS-VULN-29-004 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-cli.md` with command reference, samples, exit codes, CI snippets. Dependencies: DOCS-VULN-29-003. | Needs policy overlay inputs | DOVL0101 | -| DOCS-VULN-29-005 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Write `/docs/vuln/findings-ledger.md` detailing event schema, hashing, Merkle roots, replay tooling. Dependencies: DOCS-VULN-29-004. | Requires security review | DOVL0101 | -| DOCS-VULN-29-006 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Update `/docs/policy/vuln-determinations.md` for new rationale, signals, simulation semantics. Dependencies: DOCS-VULN-29-005. | Depends on DevOps rollout plan | DOVL0101 | -| DOCS-VULN-29-007 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Publish `/docs/vex/explorer-integration.md` covering CSAF mapping, suppression precedence, status semantics. Dependencies: DOCS-VULN-29-006. | Needs CLI examples (132_CLCI0110) | DOVL0101 | -| DOCS-VULN-29-008 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Export Center Guild | docs/modules/vuln-explorer | Publish `/docs/advisories/explorer-integration.md` covering key normalization, withdrawn handling, provenance. Dependencies: DOCS-VULN-29-007. | Need export bundle spec | DOVL0102 | -| DOCS-VULN-29-009 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Author `/docs/sbom/vuln-resolution.md` detailing version semantics, scope, paths, safe version hints. Dependencies: DOCS-VULN-29-008. | Depends on #1 | DOVL0102 | -| DOCS-VULN-29-010 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Publish `/docs/observability/vuln-telemetry.md` (metrics, logs, tracing, dashboards, SLOs). Dependencies: DOCS-VULN-29-009. | Requires DevOps automation plan | DOVL0102 | -| DOCS-VULN-29-011 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Notifications Guild | docs/modules/vuln-explorer | Create `/docs/security/vuln-rbac.md` for roles, ABAC policies, attachment encryption, CSRF. Dependencies: DOCS-VULN-29-010. | Needs notifications contract | DOVL0102 | -| DOCS-VULN-29-012 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Write `/docs/runbooks/vuln-ops.md` (projector lag, resolver storms, export failures, policy activation). Dependencies: DOCS-VULN-29-011. | Requires policy overlay outputs | DOVL0102 | -| DOCS-VULN-29-013 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API images, manifests, resource sizing, health checks. Dependencies: DOCS-VULN-29-012. | Needs CLI/export scripts from 132_CLCI0110 | DOVL0102 | +| DOCS-VULN-29-001 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-overview.md` covering domain model, identities, AOC guarantees, workflow summary. | Need GRAP0101 contract | DOVL0101 | +| DOCS-VULN-29-002 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Write `/docs/vuln/explorer-using-console.md` with workflows, screenshots, keyboard shortcuts, saved views, deep links. Dependencies: DOCS-VULN-29-001. | Depends on #1 | DOVL0101 | +| DOCS-VULN-29-003 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · UI Guild | docs/modules/vuln-explorer | Author `/docs/vuln/explorer-api.md` (endpoints, query schema, grouping, errors, rate limits). Dependencies: DOCS-VULN-29-002. | Requires UI assets | DOVL0101 | +| DOCS-VULN-29-004 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-cli.md` with command reference, samples, exit codes, CI snippets. Dependencies: DOCS-VULN-29-003. | Needs policy overlay inputs | DOVL0101 | +| DOCS-VULN-29-005 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Write `/docs/vuln/findings-ledger.md` detailing event schema, hashing, Merkle roots, replay tooling. Dependencies: DOCS-VULN-29-004. | Requires security review | DOVL0101 | +| DOCS-VULN-29-006 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Update `/docs/policy/vuln-determinations.md` for new rationale, signals, simulation semantics. Dependencies: DOCS-VULN-29-005. | Depends on DevOps rollout plan | DOVL0101 | +| DOCS-VULN-29-007 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Publish `/docs/vex/explorer-integration.md` covering CSAF mapping, suppression precedence, status semantics. Dependencies: DOCS-VULN-29-006. | Needs CLI examples (132_CLCI0110) | DOVL0101 | +| DOCS-VULN-29-008 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Export Center Guild | docs/modules/vuln-explorer | Publish `/docs/advisories/explorer-integration.md` covering key normalization, withdrawn handling, provenance. Dependencies: DOCS-VULN-29-007. | Need export bundle spec | DOVL0102 | +| DOCS-VULN-29-009 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Author `/docs/sbom/vuln-resolution.md` detailing version semantics, scope, paths, safe version hints. Dependencies: DOCS-VULN-29-008. | Depends on #1 | DOVL0102 | +| DOCS-VULN-29-010 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Publish `/docs/observability/vuln-telemetry.md` (metrics, logs, tracing, dashboards, SLOs). Dependencies: DOCS-VULN-29-009. | Requires DevOps automation plan | DOVL0102 | +| DOCS-VULN-29-011 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Notifications Guild | docs/modules/vuln-explorer | Create `/docs/security/vuln-rbac.md` for roles, ABAC policies, attachment encryption, CSRF. Dependencies: DOCS-VULN-29-010. | Needs notifications contract | DOVL0102 | +| DOCS-VULN-29-012 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Write `/docs/runbooks/vuln-ops.md` (projector lag, resolver storms, export failures, policy activation). Dependencies: DOCS-VULN-29-011. | Requires policy overlay outputs | DOVL0102 | +| DOCS-VULN-29-013 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API images, manifests, resource sizing, health checks. Dependencies: DOCS-VULN-29-012. | Needs CLI/export scripts from 132_CLCI0110 | DOVL0102 | | DOWNLOADS-CONSOLE-23-001 | TODO | | SPRINT_0502_0001_0001_ops_deployment_ii | Docs Guild · Deployment Guild | docs/console | Maintain signed downloads manifest pipeline (images, Helm, offline bundles), publish JSON under `deploy/downloads/manifest.json`, and document sync cadence for Console + docs parity. | Need latest console build instructions | DOCN0101 | | DPOP-11-001 | TODO | 2025-11-08 | SPRINT_100_identity_signing | Docs Guild · Authority Core | src/Authority/StellaOps.Authority | Need DPoP ADR from PGMI0101 | AUTH-AOC-19-002 | DODP0101 | | DSL-401-005 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · Policy Guild | `docs/policy/dsl.md`, `docs/policy/lifecycle.md` | Depends on PLLG0101 DSL updates | Depends on PLLG0101 DSL updates | DODP0101 | @@ -1360,14 +1360,14 @@ | POLICY-27-004 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add publish/promote/rollback/sign commands with attestation checks and canary args. | POLICY-27-003 | CLPS0102 | | POLICY-27-005 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild · Docs Guild | src/Cli/StellaOps.Cli | Update CLI docs/samples for Policy Studio (JSON schemas, exit codes, CI snippets). | POLICY-27-004 | CLPS0102 | | POLICY-27-006 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Update CLI policy scopes/help text to request new Policy Studio scopes and adjust regression tests. | POLICY-27-005 | CLPS0102 | -| POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, DevEx/CLI Guild (docs) | | | | | -| POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | -| POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Security Guild (docs) | | | | | -| POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Architecture Guild (docs) | | | | | -| POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Observability Guild (docs) | | | | | -| POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Ops Guild (docs) | | | | | -| POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Policy Guild (docs) | | | | | -| POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | +| POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, DevEx/CLI Guild (docs) | | | | | +| POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | +| POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Security Guild (docs) | | | | | +| POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Architecture Guild (docs) | | | | | +| POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Observability Guild (docs) | | | | | +| POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Ops Guild (docs) | | | | | +| POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Policy Guild (docs) | | | | | +| POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | | POLICY-401-026 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild · Concelier Guild (`docs/policy/dsl.md`, `docs/uncertainty/README.md`) | `docs/policy/dsl.md`, `docs/uncertainty/README.md` | | | | | POLICY-AIRGAP-56-001 | TODO | | SPRINT_123_policy_reasoning | Policy Guild | src/Policy/StellaOps.Policy.Engine | Support policy pack imports from mirror bundles, track `bundle_id` metadata, deterministic caching. | OFFK0101 | POAI0101 | | POLICY-AIRGAP-56-002 | TODO | | SPRINT_123_policy_reasoning | Policy Guild · Policy Studio Guild | src/Policy/StellaOps.Policy.Engine | Export policy sub-bundles with version metadata + checksums. | POLICY-AIRGAP-56-001 | POAI0101 | @@ -1532,7 +1532,7 @@ | RISK-67-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | | RISK-67-002 | TODO | | SPRINT_128_policy_reasoning | Policy Guild / src/Policy/StellaOps.Policy.Engine | src/Policy/StellaOps.Policy.Engine | | POLICY-RISK-67-001 | | | RISK-67-003 | BLOCKED (2025-11-26) | | SPRINT_0128_0001_0001_policy_reasoning | Policy Guild, Risk Engine Guild / src/Policy/__Libraries/StellaOps.Policy | src/Policy/__Libraries/StellaOps.Policy | | POLICY-RISK-67-002 | Blocked by missing risk profile schema + lifecycle API contract. | -| RISK-67-004 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, CLI Guild (docs) | | | | | +| RISK-67-004 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, CLI Guild (docs) | | | | | | RISK-68-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild, Policy Studio Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | | RISK-68-002 | TODO | | SPRINT_128_policy_reasoning | Risk Profile Schema Guild / src/Policy/StellaOps.Policy.RiskProfile | src/Policy/StellaOps.Policy.RiskProfile | | POLICY-RISK-68-001 | | | RISK-69-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild, Notifications Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | @@ -1556,7 +1556,7 @@ | RISK-ENGINE-70-002 | TODO | | SPRINT_0129_0001_0001_policy_reasoning | Risk Engine Guild, Observability Guild / src/RiskEngine/StellaOps.RiskEngine | src/RiskEngine/StellaOps.RiskEngine | Integrate runtime evidence provider and reachability provider outputs with caching + TTL | RISK-ENGINE-70-001 | | | RULES-33-001 | REVIEW (2025-10-30) | 2025-10-30 | SPRINT_0506_0001_0001_ops_devops_iv | DevOps Guild, Platform Leads (ops/devops) | ops/devops | | | | | RUNBOOK-401-017 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · Ops Guild (`docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md`) | `docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md` | | | | -| RUNBOOK-55-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, Ops Guild (docs) | | | | | +| RUNBOOK-55-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, Ops Guild (docs) | | | | | | RUNBOOK-REPLAY-187-004 | TODO | | SPRINT_160_export_evidence | Docs/Ops Guild · `/docs/runbooks/replay_ops.md` | docs/runbooks/replay_ops.md | Docs/Ops Guild · `/docs/runbooks/replay_ops.md` | | | | RUNTIME-401-002 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Signals Guild (`src/Signals/StellaOps.Signals`) | `src/Signals/StellaOps.Signals` | | | | | RUNTIME-PROBE-401-010 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Runtime Signals Guild (`src/Signals/StellaOps.Signals.Runtime`, `ops/probes`) | `src/Signals/StellaOps.Signals.Runtime`, `ops/probes` | Implement lightweight runtime probes (EventPipe/.NET, JFR/JVM) that capture method enter events for the target components, package them as CAS traces, and feed them into the Signals ingestion pipeline. | | | @@ -1675,13 +1675,13 @@ | SCANNER-ANALYZERS-RUBY-28-010 | TODO | | SPRINT_135_scanner_surface | Ruby Analyzer Guild, Signals Guild (src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby) | src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby | Optional runtime evidence integration (if provided logs/metrics) with path hashing, without altering static precedence. | SCANNER-ANALYZERS-RUBY-28-009 | | | SCANNER-ANALYZERS-RUBY-28-011 | TODO | | SPRINT_135_scanner_surface | Ruby Analyzer Guild, DevOps Guild (src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby) | src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby | Package analyzer plug-in, add CLI (`stella ruby inspect`), refresh Offline Kit documentation. | SCANNER-ANALYZERS-RUBY-28-010 | | | SCANNER-ANALYZERS-RUBY-28-012 | TODO | | SPRINT_135_scanner_surface | Ruby Analyzer Guild (src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby) | src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby | Policy signal emitter: rubygems drift, native extension flags, dangerous constructs counts, TLS verify posture, dynamic require eval warnings. | SCANNER-ANALYZERS-RUBY-28-011 | | -| SCANNER-BENCH-62-002 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | -| SCANNER-BENCH-62-003 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | -| SCANNER-BENCH-62-004 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Java Analyzer Guild (docs) | | | | | -| SCANNER-BENCH-62-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Go Analyzer Guild (docs) | | | | | -| SCANNER-BENCH-62-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Rust Analyzer Guild (docs) | | | | | -| SCANNER-BENCH-62-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, EntryTrace Guild (docs) | | | | | -| SCANNER-BENCH-62-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | | +| SCANNER-BENCH-62-002 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | +| SCANNER-BENCH-62-003 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | +| SCANNER-BENCH-62-004 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Java Analyzer Guild (docs) | | | | | +| SCANNER-BENCH-62-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Go Analyzer Guild (docs) | | | | | +| SCANNER-BENCH-62-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Rust Analyzer Guild (docs) | | | | | +| SCANNER-BENCH-62-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, EntryTrace Guild (docs) | | | | | +| SCANNER-BENCH-62-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | | | SCANNER-CLI-0001 | DONE | 2025-11-10 | SPRINT_0138_0000_0001_scanner_ruby_parity | CLI Guild, Ruby Analyzer Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | Coordinate CLI UX/help text for new Ruby verbs and update CLI docs/golden outputs. | SCANNER-ENG-0019 | | | SCANNER-DET-01 | DONE (2025-12-03) | 2025-12-03 | SPRINT_0301_0001_0001_docs_md_i | Docs Guild · Scanner Guild | | Deterministic compose fixtures landed; docs published. | | | SCANNER-DOCS-0003 | TODO | | SPRINT_327_docs_modules_scanner | Docs Guild, Product Guild (docs/modules/scanner) | docs/modules/scanner | Gather Windows/macOS analyzer demand signals and record findings in `docs/benchmarks/scanner/windows-macos-demand.md` for marketing + product readiness. | | | @@ -1775,7 +1775,7 @@ | SDKREL-63-002 | TODO | | SPRINT_0208_0001_0001_sdk | SDK Release Guild, API Governance Guild (src/Sdk/StellaOps.Sdk.Release) | src/Sdk/StellaOps.Sdk.Release | Integrate changelog automation pulling from OAS diffs and generator metadata. Dependencies: SDKREL-63-001. | | | | SDKREL-64-001 | TODO | | SPRINT_0208_0001_0001_sdk | SDK Release Guild, Notifications Guild (src/Sdk/StellaOps.Sdk.Release) | src/Sdk/StellaOps.Sdk.Release | Hook SDK releases into Notifications Studio with scoped announcements and RSS/Atom feeds. Dependencies: SDKREL-63-002. | | | | SDKREL-64-002 | TODO | | SPRINT_0208_0001_0001_sdk | SDK Release Guild, Export Center Guild (src/Sdk/StellaOps.Sdk.Release) | src/Sdk/StellaOps.Sdk.Release | Add `devportal --offline` bundle job packaging docs, specs, SDK artifacts for air-gapped users. Dependencies: SDKREL-64-001. | | | -| SEC-62-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, Authority Core (docs) | | | | | +| SEC-62-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, Authority Core (docs) | | | | | | SEC-CRYPTO-90-001 | DONE | 2025-11-07 | SPRINT_514_sovereign_crypto_enablement | Security Guild (src/__Libraries/StellaOps.Cryptography) | src/__Libraries/StellaOps.Cryptography | Produce the RootPack_RU implementation plan, provider strategy (CryptoPro + PKCS#11), and backlog split for sovereign crypto work. | | | | SEC-CRYPTO-90-002 | DONE | 2025-11-07 | SPRINT_514_sovereign_crypto_enablement | Security Guild (src/__Libraries/StellaOps.Cryptography) | src/__Libraries/StellaOps.Cryptography | Extend signature/catalog constants and configuration schema to recognize `GOST12-256/512`, regional crypto profiles, and provider preference ordering. | | | | SEC-CRYPTO-90-003 | DONE | 2025-11-07 | SPRINT_514_sovereign_crypto_enablement | Security Guild (src/__Libraries/StellaOps.Cryptography) | src/__Libraries/StellaOps.Cryptography | Implement `StellaOps.Cryptography.Plugin.CryptoPro` provider (sign/verify/JWK export) using CryptoPro CSP with deterministic logging/tests. | | | @@ -1797,7 +1797,7 @@ | SEC-CRYPTO-90-019 | TODO | | SPRINT_514_sovereign_crypto_enablement | Security Guild | third_party/forks/AlexMAS.GostCryptography | Patch the fork to drop vulnerable `System.Security.Cryptography.{Pkcs,Xml}` 6.0.0 dependencies (target .NET 8+, adopt fixed BCL packages, re-run tests). | Needs fork validation | CRSA0101 | | SEC-CRYPTO-90-020 | TODO | | SPRINT_514_sovereign_crypto_enablement | Security Guild | src/__Libraries/StellaOps.Cryptography.Plugin.CryptoPro | Re-point `StellaOps.Cryptography.Plugin.CryptoPro` to the forked sources (replace NuGet package references, adjust DI wiring) and prove the plugin works end-to-end. | Depends on #5 | CRSA0101 | | SEC-CRYPTO-90-021 | BLOCKED (2025-11-27) | Windows CSP runner pending (depends on 90-020) | SPRINT_514_sovereign_crypto_enablement | Security + QA Guilds | scripts/crypto/**, docs/security/rootpack_ru_validation.md | Validate the forked library + plugin on both Windows (CryptoPro CSP) and Linux (OpenSSL GOST fallback) builds/tests; document any platform-specific prerequisites. | Depends on #6 | CRSA0101 | -| SEC-OBS-50-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, Security Guild (docs) | | | | | +| SEC-OBS-50-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, Security Guild (docs) | | | | | | SEC2 | DONE | 2025-11-09 | SPRINT_100_identity_signing | Security Guild, Storage Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard) | src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard | | | | | SEC3 | DONE | 2025-11-09 | SPRINT_100_identity_signing | Security Guild, BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard) | src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard | | | | | SEC5 | DONE | 2025-11-09 | SPRINT_100_identity_signing | Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard) | src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard | | | | @@ -1822,10 +1822,10 @@ | SIG-26-002 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | SIG-26-003 | TODO | | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/UI/StellaOps.UI) | src/UI/StellaOps.UI | | | | | SIG-26-004 | TODO | | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/UI/StellaOps.UI) | src/UI/StellaOps.UI | | | | -| SIG-26-005 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, UI Guild (docs) | | | | | -| SIG-26-006 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, DevEx/CLI Guild (docs) | | | | | -| SIG-26-007 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, BE-Base Platform Guild (docs) | | | | | -| SIG-26-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | | +| SIG-26-005 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, UI Guild (docs) | | | | | +| SIG-26-006 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, DevEx/CLI Guild (docs) | | | | | +| SIG-26-007 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, BE-Base Platform Guild (docs) | | | | | +| SIG-26-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | | | SIG-STORE-401-016 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Signals Guild · BE-Base Platform Guild (`src/Signals/StellaOps.Signals`, `src/__Libraries/StellaOps.Replay.Core`) | `src/Signals/StellaOps.Signals`, `src/__Libraries/StellaOps.Replay.Core` | Introduce shared reachability store collections (`func_nodes`, `call_edges`, `cve_func_hits`), indexes, and repository APIs so Scanner/Signals/Policy can reuse canonical function data. | | | | SIGN-CORE-186-004 | DONE | 2025-11-26 | SPRINT_186_record_deterministic_execution | Signing Guild | `src/Signer/StellaOps.Signer`, `src/__Libraries/StellaOps.Cryptography` | Replace the HMAC demo implementation in `StellaOps.Signer` with StellaOps.Cryptography providers (keyless + KMS), including provider selection, key material loading, and cosign-compatible DSSE signature output. | Mirrors #1 | SIGR0101 | | SIGN-CORE-186-005 | DONE | 2025-11-26 | SPRINT_186_record_deterministic_execution | Signing Guild | `src/Signer/StellaOps.Signer.Core` | Refactor `SignerStatementBuilder` to support StellaOps predicate types (e.g., `stella.ops/promotion@v1`) and delegate payload canonicalisation to the Provenance library once available. | Mirrors #2 | SIGR0101 | @@ -1964,7 +1964,7 @@ | TEN-48-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | | TEN-49-001 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | TEST-186-006 | TODO | | SPRINT_186_record_deterministic_execution | Signing Guild, QA Guild (`src/Signer/StellaOps.Signer.Tests`) | `src/Signer/StellaOps.Signer.Tests` | | | | -| TEST-62-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Contract Testing Guild (docs) | | | | | +| TEST-62-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Contract Testing Guild (docs) | | | | | | TIME-57-001 | TODO | | SPRINT_0503_0001_0001_ops_devops_i | Exporter Guild · AirGap Time Guild · CLI Guild | | | PROGRAM-STAFF-1001 | | | TIME-57-002 | TODO | | SPRINT_510_airgap | Exporter Guild · AirGap Time Guild · CLI Guild | src/AirGap/StellaOps.AirGap.Time | PROGRAM-STAFF-1001 | PROGRAM-STAFF-1001 | AGTM0101 | | TIME-58-001 | TODO | | SPRINT_510_airgap | AirGap Time Guild | src/AirGap/StellaOps.AirGap.Time | AIRGAP-TIME-58-001 | AIRGAP-TIME-58-001 | AGTM0101 | @@ -2033,11 +2033,11 @@ | VEX-30-002 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | VEX-30-003 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | VEX-30-004 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | -| VEX-30-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Console Guild (docs) | | | | | -| VEX-30-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | DOVX0101 | +| VEX-30-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Console Guild (docs) | | | | | +| VEX-30-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | DOVX0101 | | VEX-30-007 | TODO | | SPRINT_216_web_v | BE-Base Platform Guild, VEX Lens Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | | | DOVX0101 | -| VEX-30-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Security Guild (docs) | | | | DOVX0101 | -| VEX-30-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | DOVX0101 | +| VEX-30-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Security Guild (docs) | | | | DOVX0101 | +| VEX-30-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | DOVX0101 | | VEX-401-006 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild (`src/Policy/StellaOps.Policy.Engine`, `src/Policy/__Libraries/StellaOps.Policy`) | `src/Policy/StellaOps.Policy.Engine`, `src/Policy/__Libraries/StellaOps.Policy` | | | DOVX0101 | | VEX-401-010 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild (`src/Policy/StellaOps.Policy.Engine/Vex`, `docs/modules/policy/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md`) | `src/Policy/StellaOps.Policy.Engine/Vex`, `docs/modules/policy/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md` | | | DOVX0101 | | VEX-401-011 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | verify | | | | DOVX0101 | @@ -2071,13 +2071,13 @@ | VULN-29-004 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, Observability Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | VULN-29-005 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | VULN-29-006 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild, Docs Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | -| VULN-29-007 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Excititor Guild (docs) | | | | | -| VULN-29-008 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Concelier Guild (docs) | | | | | -| VULN-29-009 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, SBOM Service Guild (docs) | | | | | -| VULN-29-010 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Observability Guild (docs) | | | | | -| VULN-29-011 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Security Guild (docs) | | | | | -| VULN-29-012 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Ops Guild (docs) | | | | | -| VULN-29-013 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Deployment Guild (docs) | | | | | +| VULN-29-007 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Excititor Guild (docs) | | | | | +| VULN-29-008 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Concelier Guild (docs) | | | | | +| VULN-29-009 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, SBOM Service Guild (docs) | | | | | +| VULN-29-010 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Observability Guild (docs) | | | | | +| VULN-29-011 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Security Guild (docs) | | | | | +| VULN-29-012 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Ops Guild (docs) | | | | | +| VULN-29-013 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Deployment Guild (docs) | | | | | | VULN-API-29-001 | DONE | 2025-11-25 | SPRINT_0129_0001_0001_policy_reasoning | Vuln Explorer API Guild / src/VulnExplorer/StellaOps.VulnExplorer.Api | src/VulnExplorer/StellaOps.VulnExplorer.Api | Define OpenAPI spec (list/detail/query/simulation/workflow/export), query JSON schema, pagination/grouping contracts, and error codes | | PLVA0101 | | VULN-API-29-002 | DONE | 2025-11-25 | SPRINT_0129_0001_0001_policy_reasoning | Vuln Explorer API Guild / src/VulnExplorer/StellaOps.VulnExplorer.Api | src/VulnExplorer/StellaOps.VulnExplorer.Api | Implement list/query endpoints with policy parameter, grouping, server paging, caching, and cost budgets; tests at `tests/TestResults/vuln-explorer/api.trx`. | VULN-API-29-001 | PLVA0101 | | VULN-API-29-003 | DONE | 2025-11-25 | SPRINT_0129_0001_0001_policy_reasoning | Vuln Explorer API Guild / src/VulnExplorer/StellaOps.VulnExplorer.Api | src/VulnExplorer/StellaOps.VulnExplorer.Api | Implement detail endpoint aggregating evidence, policy rationale, paths | VULN-API-29-002 | PLVA0101 | @@ -2624,11 +2624,11 @@ | CONCELIER-GRAPH-28-102 | TODO | | SPRINT_113_concelier_ii | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Add batch fetch endpoints keyed by component sets so graph tooltips can pull raw observations/linksets efficiently; include provenance + timestamps but no derived severity. Depends on CONCELIER-GRAPH-24-101. | Depends on #1 | CCGH0101 | | CONCELIER-LNM-21-001 | DONE | 2025-11-17 | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Define the immutable `advisory_observations` model (per-source fields, version ranges, severity text, provenance metadata, tenant guards) so every ingestion path records raw statements without merge artifacts. | Needs Link-Not-Merge approval (005_ATLN0101) | AGCN0101 | | CONCELIER-LNM-21-002 | DONE | 2025-11-22 | SPRINT_113_concelier_ii | Concelier Core Guild · Data Science Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Implement correlation pipelines (alias graph, purl overlap, CVSS vector compare) that output linksets with confidence scores + conflict markers, never collapsing conflicting facts into single values. Depends on CONCELIER-LNM-21-001. | Depends on #7 for precedence rules | AGCN0101 | -| CONCELIER-LNM-21-003 | BLOCKED | 2025-11-18 | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Record disagreements (severity, CVSS, references) on linksets as structured conflict entries so consumers can reason about divergence without Concelier resolving it. Depends on CONCELIER-LNM-21-002. | Requires #8 heuristics | AGCN0101 | -| CONCELIER-LNM-21-004 | TODO | | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Delete legacy merge/dedup logic, add guardrails/tests to keep ingestion append-only, and document how linksets supersede the old merge outputs. Depends on CONCELIER-LNM-21-003. | Depends on #9 | AGCN0101 | -| CONCELIER-LNM-21-005 | TODO | | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Emit `advisory.linkset.updated` events containing delta descriptions + observation ids so downstream evaluators can subscribe deterministically. Depends on CONCELIER-LNM-21-004. | Requires CCLN0101 store changes | CCCO0101 | -| CONCELIER-LNM-21-101 | TODO | | SPRINT_113_concelier_ii | Concelier Storage Guild | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo | Provision the Mongo collections (`advisory_observations`, `advisory_linksets`) with hashed shard keys, tenant indexes, and TTL for ingest metadata to support Link-Not-Merge at scale. Depends on CONCELIER-LNM-21-005. | Wait for schema freeze | CCLN0101 | -| CONCELIER-LNM-21-102 | TODO | | SPRINT_113_concelier_ii | Concelier Storage Guild · DevOps Guild | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo | Backfill legacy merged advisories into the new observation/linkset collections, seed tombstones for deprecated docs, and provide rollback tooling for Offline Kit operators. Depends on CONCELIER-LNM-21-101. | Depends on #1 | CCLN0101 | +| CONCELIER-LNM-21-003 | DONE | 2025-11-22 | SPRINT_0113_0001_0002_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Record disagreements (severity, CVSS, references) on linksets as structured conflict entries so consumers can reason about divergence without Concelier resolving it. Depends on CONCELIER-LNM-21-002. | Completed | AGCN0101 | +| CONCELIER-LNM-21-004 | DONE | 2025-11-27 | SPRINT_0113_0001_0002_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Delete legacy merge/dedup logic, add guardrails/tests to keep ingestion append-only, and document how linksets supersede the old merge outputs. Depends on CONCELIER-LNM-21-003. | Completed | AGCN0101 | +| CONCELIER-LNM-21-005 | DONE | 2025-11-27 | SPRINT_0113_0001_0002_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Emit `advisory.linkset.updated` events containing delta descriptions + observation ids so downstream evaluators can subscribe deterministically. Depends on CONCELIER-LNM-21-004. | Completed | CCCO0101 | +| CONCELIER-LNM-21-101 | DONE | 2025-11-27 | SPRINT_0113_0001_0002_concelier_ii | Concelier Storage Guild | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo | Provision the Mongo collections (`advisory_observations`, `advisory_linksets`) with hashed shard keys, tenant indexes, and TTL for ingest metadata to support Link-Not-Merge at scale. Depends on CONCELIER-LNM-21-005. | Completed | CCLN0101 | +| CONCELIER-LNM-21-102 | DONE | 2025-11-28 | SPRINT_0113_0001_0002_concelier_ii | Concelier Storage Guild · DevOps Guild | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo | Backfill legacy merged advisories into the new observation/linkset collections, seed tombstones for deprecated docs, and provide rollback tooling for Offline Kit operators. Depends on CONCELIER-LNM-21-101. | Completed | CCLN0101 | | CONCELIER-LNM-21-103 | TODO | | SPRINT_113_concelier_ii | Concelier Storage Guild (src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo) | src/Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo | Move large raw payloads to object storage with deterministic pointers, update bootstrapper/offline kit seeds, and guarantee provenance metadata remains intact. Depends on CONCELIER-LNM-21-102. | — | ATLN0101 | | CONCELIER-LNM-21-201 | TODO | | SPRINT_113_concelier_ii | Concelier WebService Guild · Platform Guild | src/Concelier/StellaOps.Concelier.WebService | Add `/advisories/observations` with filters for alias/purl/source plus strict tenant scopes; responses must only echo upstream values + provenance fields. Depends on CONCELIER-LNM-21-103. | Wait for storage sprint (CCLN0101) | CCLN0102 | | CONCELIER-LNM-21-202 | TODO | | SPRINT_113_concelier_ii | Concelier WebService Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | Implement `/advisories/linksets`/`export`/`evidence` endpoints surfacing correlation + conflict payloads and `ERR_AGG_*` error mapping, never exposing synthesis/merge results. Depends on CONCELIER-LNM-21-201. | — | ATLN0101 | @@ -2961,81 +2961,81 @@ | DOCS-POLICY-27-003 | BLOCKED | 2025-10-27 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Policy Registry Guild | docs/policy/lifecycle.md | Document `/docs/policy/versioning-and-publishing.md` (semver rules, attestations, rollback) with compliance checklist. Dependencies: DOCS-POLICY-27-002. | Requires registry schema from CCWO0101 | DOPL0102 | | DOCS-POLICY-27-004 | BLOCKED | 2025-10-27 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Scheduler Guild | docs/policy/lifecycle.md | Write `/docs/policy/simulation.md` covering quick vs batch sim, thresholds, evidence bundles, CLI examples. Dependencies: DOCS-POLICY-27-003. | Depends on scheduler hooks from 050_DEVL0101 | DOPL0102 | | DOCS-POLICY-27-005 | BLOCKED | 2025-10-27 | SPRINT_307_docs_tasks_md_vii | Docs Guild · Product Ops | docs/policy/lifecycle.md | Publish `/docs/policy/review-and-approval.md` with approver requirements, comments, webhooks, audit trail guidance. Dependencies: DOCS-POLICY-27-004. | Await product ops approvals | DOPL0102 | -| DOCS-POLICY-27-006 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Author `/docs/policy/promotion.md` covering environments, canary, rollback, and monitoring steps. Dependencies: DOCS-POLICY-27-005. | Need RLS decision from PLLG0104 | DOPL0103 | -| DOCS-POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · CLI Guild | docs/policy/runs.md | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, and compliance checklist. Dependencies: DOCS-POLICY-27-006. | Requires CLI samples from 132_CLCI0110 | DOPL0103 | -| DOCS-POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Publish `/docs/policy/api.md` describing Registry endpoints, request/response schemas, errors, and feature flags. Dependencies: DOCS-POLICY-27-007. | Waiting on registry schema (CCWO0101) | DOPL0103 | -| DOCS-POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Security Guild | docs/policy/runs.md | Create `/docs/security/policy-attestations.md` (signing, verification, rotation). | Needs security review | POKT0101 | -| DOCS-POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Architecture Guild | docs/policy/runs.md | Author `/docs/modules/policy/registry-architecture.md` (service design, schemas, failure modes). | Requires architecture review minutes | POKT0101 | -| DOCS-POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Observability Guild | docs/policy/runs.md | Publish `/docs/observability/policy-telemetry.md` with metrics/log tables, dashboards, alerts, and compliance checklist. Dependencies: DOCS-POLICY-27-010. | Requires observability hooks from 066_PLOB0101 | DOPL0103 | -| DOCS-POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Ops Guild | docs/policy/runs.md | Write `/docs/runbooks/policy-incident.md` detailing rollback, freeze, forensic steps, notifications. Dependencies: DOCS-POLICY-27-011. | Needs ops playbooks (DVDO0108) | DOPL0103 | -| DOCS-POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Update `/docs/examples/policy-templates.md` with new templates, snippets, and sample policies. Dependencies: DOCS-POLICY-27-012. | Await policy guild approval | DOPL0103 | -| DOCS-POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Refresh `/docs/aoc/aoc-guardrails.md` to include Studio-specific guardrails and validation scenarios. Dependencies: DOCS-POLICY-27-013. | Needs policy registry approvals | DOPL0103 | +| DOCS-POLICY-27-006 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Author `/docs/policy/promotion.md` covering environments, canary, rollback, and monitoring steps. Dependencies: DOCS-POLICY-27-005. | Need RLS decision from PLLG0104 | DOPL0103 | +| DOCS-POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · CLI Guild | docs/policy/runs.md | Update `/docs/policy/cli.md` with new commands, JSON schemas, CI usage, and compliance checklist. Dependencies: DOCS-POLICY-27-006. | Requires CLI samples from 132_CLCI0110 | DOPL0103 | +| DOCS-POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Publish `/docs/policy/api.md` describing Registry endpoints, request/response schemas, errors, and feature flags. Dependencies: DOCS-POLICY-27-007. | Waiting on registry schema (CCWO0101) | DOPL0103 | +| DOCS-POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Security Guild | docs/policy/runs.md | Create `/docs/security/policy-attestations.md` (signing, verification, rotation). | Needs security review | POKT0101 | +| DOCS-POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Architecture Guild | docs/policy/runs.md | Author `/docs/modules/policy/registry-architecture.md` (service design, schemas, failure modes). | Requires architecture review minutes | POKT0101 | +| DOCS-POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Observability Guild | docs/policy/runs.md | Publish `/docs/observability/policy-telemetry.md` with metrics/log tables, dashboards, alerts, and compliance checklist. Dependencies: DOCS-POLICY-27-010. | Requires observability hooks from 066_PLOB0101 | DOPL0103 | +| DOCS-POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Ops Guild | docs/policy/runs.md | Write `/docs/runbooks/policy-incident.md` detailing rollback, freeze, forensic steps, notifications. Dependencies: DOCS-POLICY-27-011. | Needs ops playbooks (DVDO0108) | DOPL0103 | +| DOCS-POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/policy/runs.md | Update `/docs/examples/policy-templates.md` with new templates, snippets, and sample policies. Dependencies: DOCS-POLICY-27-012. | Await policy guild approval | DOPL0103 | +| DOCS-POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Registry Guild | docs/policy/runs.md | Refresh `/docs/aoc/aoc-guardrails.md` to include Studio-specific guardrails and validation scenarios. Dependencies: DOCS-POLICY-27-013. | Needs policy registry approvals | DOPL0103 | | DOCS-POLICY-DET-01 | DONE (2025-11-23) | 2025-11-23 | SPRINT_0301_0001_0001_docs_md_i | Docs Guild · Policy Guild | docs/policy/runs.md | Extend `docs/modules/policy/architecture.md` with determinism gate semantics and provenance references. | Depends on deterministic harness (137_SCDT0101) | DOPL0103 | | DOCS-PROMO-70-001 | DONE (2025-11-26) | 2025-11-26 | SPRINT_304_docs_tasks_md_iv | Docs Guild · Provenance Guild | docs/release/promotion-attestations.md | Publish `/docs/release/promotion-attestations.md` describing the promotion workflow (CLI commands, Signer/Attestor integration, offline verification) and update `/docs/forensics/provenance-attestation.md` with the new predicate. Dependencies: PROV-OBS-53-003, CLI-PROMO-70-002. | — | DOPV0101 | | DOCS-REACH-201-006 | TODO | | SPRINT_400_runtime_facts_static_callgraph_union | Docs Guild · Runtime Evidence Guild | docs/reachability | Author the reachability doc set (`docs/signals/reachability.md`, `callgraph-formats.md`, `runtime-facts.md`, CLI/UI appendices) plus update Zastava + Replay guides with the new evidence and operators’ workflow. | Needs RBRE0101 provenance hook summary | DORC0101 | | DOCS-REPLAY-185-003 | TODO | | SPRINT_185_shared_replay_primitives | Docs Guild · Platform Data Guild | docs/replay | Author `docs/data/replay_schema.md` detailing `replay_runs`, `replay_bundles`, `replay_subjects` collections, index guidance, and offline sync strategy aligned with Replay CAS. | Need RPRC0101 API freeze | DORR0101 | | DOCS-REPLAY-185-004 | TODO | | SPRINT_185_shared_replay_primitives | Docs Guild | docs/replay | Expand `docs/replay/DEVS_GUIDE_REPLAY.md` with integration guidance for consuming services (Scanner, Evidence Locker, CLI) and add checklist derived from `docs/replay/DETERMINISTIC_REPLAY.md` Section 11. | Depends on #1 | DORR0101 | | DOCS-REPLAY-186-004 | TODO | | SPRINT_186_record_deterministic_execution | Docs Guild · Runtime Evidence Guild | docs/replay | Author `docs/replay/TEST_STRATEGY.md` (golden replay, feed drift, tool upgrade) and link it from both replay docs and Scanner architecture pages. | Requires deterministic evidence from RBRE0101 | DORR0101 | -| DOCS-RISK-66-001 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Profile Schema Guild | docs/risk | Publish `/docs/risk/overview.md` covering concepts and glossary. | Need schema approvals from PLLG0104 | DORS0101 | -| DOCS-RISK-66-002 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/risk | Author `/docs/risk/profiles.md` (authoring, versioning, scope). Dependencies: DOCS-RISK-66-001. | Depends on #1 | DORS0101 | -| DOCS-RISK-66-003 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/factors.md` cataloging signals, transforms, reducers, TTLs. Dependencies: DOCS-RISK-66-002. | Requires engine contract from Risk Engine Guild | DORS0101 | -| DOCS-RISK-66-004 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Create `/docs/risk/formulas.md` detailing math, normalization, gating, severity. Dependencies: DOCS-RISK-66-003. | Needs engine rollout notes | DORS0101 | -| DOCS-RISK-67-001 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/explainability.md` showing artifact schema and UI screenshots. Dependencies: DOCS-RISK-66-004. | Wait for engine metrics from 066_PLOB0101 | DORS0101 | -| DOCS-RISK-67-002 | TODO | | SPRINT_308_docs_tasks_md_viii | Docs Guild · API Guild | docs/risk | Produce `/docs/risk/api.md` with endpoint reference/examples. Dependencies: DOCS-RISK-67-001. | Requires API publishing workflow | DORS0101 | -| DOCS-RISK-67-003 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Console Guild | docs/risk | Document `/docs/console/risk-ui.md` for authoring, simulation, dashboards. Dependencies: DOCS-RISK-67-002. | Needs console overlay decision | DORS0101 | -| DOCS-RISK-67-004 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/risk | Publish `/docs/modules/cli/guides/risk.md` covering CLI workflows. Dependencies: DOCS-RISK-67-003. | Requires CLI samples from 132_CLCI0110 | DORS0101 | -| DOCS-RISK-68-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Export Guild | docs/risk | Add `/docs/airgap/risk-bundles.md` for offline factor bundles. Dependencies: DOCS-RISK-67-004. | Wait for export contract (069_AGEX0101) | DORS0101 | -| DOCS-RISK-68-002 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/risk | Update `/docs/security/aoc-invariants.md` with risk scoring provenance guarantees. Dependencies: DOCS-RISK-68-001. | Requires security approvals | DORS0101 | +| DOCS-RISK-66-001 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Profile Schema Guild | docs/risk | Publish `/docs/risk/overview.md` covering concepts and glossary. | Need schema approvals from PLLG0104 | DORS0101 | +| DOCS-RISK-66-002 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Policy Guild | docs/risk | Author `/docs/risk/profiles.md` (authoring, versioning, scope). Dependencies: DOCS-RISK-66-001. | Depends on #1 | DORS0101 | +| DOCS-RISK-66-003 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/factors.md` cataloging signals, transforms, reducers, TTLs. Dependencies: DOCS-RISK-66-002. | Requires engine contract from Risk Engine Guild | DORS0101 | +| DOCS-RISK-66-004 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Create `/docs/risk/formulas.md` detailing math, normalization, gating, severity. Dependencies: DOCS-RISK-66-003. | Needs engine rollout notes | DORS0101 | +| DOCS-RISK-67-001 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · Risk Engine Guild | docs/risk | Publish `/docs/risk/explainability.md` showing artifact schema and UI screenshots. Dependencies: DOCS-RISK-66-004. | Wait for engine metrics from 066_PLOB0101 | DORS0101 | +| DOCS-RISK-67-002 | TODO | | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild · API Guild | docs/risk | Produce `/docs/risk/api.md` with endpoint reference/examples. Dependencies: DOCS-RISK-67-001. | Requires API publishing workflow | DORS0101 | +| DOCS-RISK-67-003 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Console Guild | docs/risk | Document `/docs/console/risk-ui.md` for authoring, simulation, dashboards. Dependencies: DOCS-RISK-67-002. | Needs console overlay decision | DORS0101 | +| DOCS-RISK-67-004 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/risk | Publish `/docs/modules/cli/guides/risk.md` covering CLI workflows. Dependencies: DOCS-RISK-67-003. | Requires CLI samples from 132_CLCI0110 | DORS0101 | +| DOCS-RISK-68-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Export Guild | docs/risk | Add `/docs/airgap/risk-bundles.md` for offline factor bundles. Dependencies: DOCS-RISK-67-004. | Wait for export contract (069_AGEX0101) | DORS0101 | +| DOCS-RISK-68-002 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/risk | Update `/docs/security/aoc-invariants.md` with risk scoring provenance guarantees. Dependencies: DOCS-RISK-68-001. | Requires security approvals | DORS0101 | | DOCS-RUNBOOK-401-017 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · Ops Guild | `docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md` | Publish the reachability runtime ingestion runbook, link it from delivery guides, and keep Ops/Signals troubleshooting steps current. | Need latest reachability metrics from RBBN0101 | DORU0101 | -| DOCS-RUNBOOK-55-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Ops Guild | docs/runbooks | Author `/docs/runbooks/incidents.md` describing incident mode activation, escalation steps, retention impact, verification checklist, and imposed rule banner. | Requires deployment checklist from DVPL0101 | DORU0101 | -| DOCS-SCANNER-BENCH-62-002 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture customer demand for Windows/macOS analyzer coverage and document outcomes. | Need bench inputs from SCSA0301 | DOSB0101 | -| DOCS-SCANNER-BENCH-62-003 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture Python lockfile/editable install requirements and document policy guidance. | Depends on #1 | DOSB0101 | -| DOCS-SCANNER-BENCH-62-004 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Java Analyzer Guild | docs/modules/scanner/benchmarks | Document Java lockfile ingestion guidance and policy templates. | Requires Java analyzer notes | DOSB0101 | -| DOCS-SCANNER-BENCH-62-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Go Analyzer Guild | docs/modules/scanner/benchmarks | Document Go stripped-binary fallback enrichment guidance once implementation lands. | Needs Go analyzer results | DOSB0101 | -| DOCS-SCANNER-BENCH-62-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Document Rust fingerprint enrichment guidance and policy examples. | Requires updated benchmarks from SCSA0601 | DOSB0101 | -| DOCS-SCANNER-BENCH-62-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Platform Data Guild | docs/modules/scanner/benchmarks | Publish EntryTrace explain/heuristic maintenance guide. | Wait for replay hooks (RPRC0101) | DOSB0101 | -| DOCS-SCANNER-BENCH-62-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · DevEx/CLI Guild | docs/modules/scanner/benchmarks | Produce SAST integration documentation (connector framework, policy templates). | Depends on CLI samples (132_CLCI0110) | DOSB0101 | +| DOCS-RUNBOOK-55-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Ops Guild | docs/runbooks | Author `/docs/runbooks/incidents.md` describing incident mode activation, escalation steps, retention impact, verification checklist, and imposed rule banner. | Requires deployment checklist from DVPL0101 | DORU0101 | +| DOCS-SCANNER-BENCH-62-002 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture customer demand for Windows/macOS analyzer coverage and document outcomes. | Need bench inputs from SCSA0301 | DOSB0101 | +| DOCS-SCANNER-BENCH-62-003 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Capture Python lockfile/editable install requirements and document policy guidance. | Depends on #1 | DOSB0101 | +| DOCS-SCANNER-BENCH-62-004 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Java Analyzer Guild | docs/modules/scanner/benchmarks | Document Java lockfile ingestion guidance and policy templates. | Requires Java analyzer notes | DOSB0101 | +| DOCS-SCANNER-BENCH-62-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Go Analyzer Guild | docs/modules/scanner/benchmarks | Document Go stripped-binary fallback enrichment guidance once implementation lands. | Needs Go analyzer results | DOSB0101 | +| DOCS-SCANNER-BENCH-62-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Product Guild | docs/modules/scanner/benchmarks | Document Rust fingerprint enrichment guidance and policy examples. | Requires updated benchmarks from SCSA0601 | DOSB0101 | +| DOCS-SCANNER-BENCH-62-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Platform Data Guild | docs/modules/scanner/benchmarks | Publish EntryTrace explain/heuristic maintenance guide. | Wait for replay hooks (RPRC0101) | DOSB0101 | +| DOCS-SCANNER-BENCH-62-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · DevEx/CLI Guild | docs/modules/scanner/benchmarks | Produce SAST integration documentation (connector framework, policy templates). | Depends on CLI samples (132_CLCI0110) | DOSB0101 | | DOCS-SCANNER-DET-01 | DONE (2025-12-03) | 2025-12-03 | SPRINT_0301_0001_0001_docs_md_i | Docs Guild · Scanner Guild | docs/modules/scanner/benchmarks | `/docs/modules/scanner/deterministic-sbom-compose.md` plus scan guide updates + fixture bundle (`docs/modules/scanner/fixtures/deterministic-compose/`). | Fixtures published via Sprint 0136; harness verified. | DOSB0101 | -| DOCS-SDK-62-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · SDK Generator Guild | docs/sdk | Publish `/docs/sdks/overview.md` plus language guides (`typescript.md`, `python.md`, `go.md`, `java.md`). | Need SDK toolchain notes from SDKG0101 | DOSK0101 | -| DOCS-SEC-62-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/auth-scopes.md` with OAuth2/PAT scopes, tenancy header usage. | Need security ADR from DVDO0110 | DOSE0101 | -| DOCS-SEC-OBS-50-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/redaction-and-privacy.md` to cover telemetry privacy controls, tenant opt-in debug, and imposed rule reminder. | Depends on PLOB0101 metrics | DOSE0101 | -| DOCS-SIG-26-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Write `/docs/signals/reachability.md` covering states, scores, provenance, retention. | Need SGSI0101 metrics freeze | DOSG0101 | -| DOCS-SIG-26-002 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Observability Guild | docs/modules/signals | Publish `/docs/signals/callgraph-formats.md` with schemas and validation errors. Dependencies: DOCS-SIG-26-001. | Depends on #1 | DOSG0101 | -| DOCS-SIG-26-003 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Create `/docs/signals/runtime-facts.md` detailing agent capabilities, privacy safeguards, opt-in flags. Dependencies: DOCS-SIG-26-002. | Requires SSE contract from SGSI0101 | DOSG0101 | -| DOCS-SIG-26-004 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/modules/signals | Document `/docs/policy/signals-weighting.md` for SPL predicates and weighting strategies. Dependencies: DOCS-SIG-26-003. | Needs CLI samples (132_CLCI0110) | DOSG0101 | -| DOCS-SIG-26-005 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · DevOps Guild | docs/modules/signals | Draft `/docs/ui/reachability-overlays.md` with badges, timelines, shortcuts. Dependencies: DOCS-SIG-26-004. | Wait for DevOps rollout plan | DOSG0101 | -| DOCS-SIG-26-006 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Security Guild | docs/modules/signals | Update `/docs/modules/cli/guides/reachability.md` for new commands and automation recipes. Dependencies: DOCS-SIG-26-005. | Requires security guidance (DVDO0110) | DOSG0101 | -| DOCS-SIG-26-007 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild · Policy Guild | docs/modules/signals | Publish `/docs/api/signals.md` covering endpoints, payloads, ETags, errors. Dependencies: DOCS-SIG-26-006. | Needs policy overlay from PLVL0102 | DOSG0101 | -| DOCS-SIG-26-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Notifications Guild | docs/modules/signals | Write `/docs/migration/enable-reachability.md` guiding rollout, fallbacks, monitoring. Dependencies: DOCS-SIG-26-007. | Depends on notifications hooks (058_NOTY0101) | DOSG0101 | -| DOCS-SURFACE-01 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Surface Guild | docs/modules/scanner/surface | Create `/docs/modules/scanner/scanner-engine.md` covering Surface.FS/Env/Secrets workflow between Scanner, Zastava, Scheduler, and Ops. | Need latest surface emit notes (SCANNER-SURFACE-04) | DOSS0101 | +| DOCS-SDK-62-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · SDK Generator Guild | docs/sdk | Publish `/docs/sdks/overview.md` plus language guides (`typescript.md`, `python.md`, `go.md`, `java.md`). | Need SDK toolchain notes from SDKG0101 | DOSK0101 | +| DOCS-SEC-62-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/auth-scopes.md` with OAuth2/PAT scopes, tenancy header usage. | Need security ADR from DVDO0110 | DOSE0101 | +| DOCS-SEC-OBS-50-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/security | Update `/docs/security/redaction-and-privacy.md` to cover telemetry privacy controls, tenant opt-in debug, and imposed rule reminder. | Depends on PLOB0101 metrics | DOSE0101 | +| DOCS-SIG-26-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Write `/docs/signals/reachability.md` covering states, scores, provenance, retention. | Need SGSI0101 metrics freeze | DOSG0101 | +| DOCS-SIG-26-002 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Observability Guild | docs/modules/signals | Publish `/docs/signals/callgraph-formats.md` with schemas and validation errors. Dependencies: DOCS-SIG-26-001. | Depends on #1 | DOSG0101 | +| DOCS-SIG-26-003 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Signals Guild | docs/modules/signals | Create `/docs/signals/runtime-facts.md` detailing agent capabilities, privacy safeguards, opt-in flags. Dependencies: DOCS-SIG-26-002. | Requires SSE contract from SGSI0101 | DOSG0101 | +| DOCS-SIG-26-004 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · CLI Guild | docs/modules/signals | Document `/docs/policy/signals-weighting.md` for SPL predicates and weighting strategies. Dependencies: DOCS-SIG-26-003. | Needs CLI samples (132_CLCI0110) | DOSG0101 | +| DOCS-SIG-26-005 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · DevOps Guild | docs/modules/signals | Draft `/docs/ui/reachability-overlays.md` with badges, timelines, shortcuts. Dependencies: DOCS-SIG-26-004. | Wait for DevOps rollout plan | DOSG0101 | +| DOCS-SIG-26-006 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Security Guild | docs/modules/signals | Update `/docs/modules/cli/guides/reachability.md` for new commands and automation recipes. Dependencies: DOCS-SIG-26-005. | Requires security guidance (DVDO0110) | DOSG0101 | +| DOCS-SIG-26-007 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild · Policy Guild | docs/modules/signals | Publish `/docs/api/signals.md` covering endpoints, payloads, ETags, errors. Dependencies: DOCS-SIG-26-006. | Needs policy overlay from PLVL0102 | DOSG0101 | +| DOCS-SIG-26-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Notifications Guild | docs/modules/signals | Write `/docs/migration/enable-reachability.md` guiding rollout, fallbacks, monitoring. Dependencies: DOCS-SIG-26-007. | Depends on notifications hooks (058_NOTY0101) | DOSG0101 | +| DOCS-SURFACE-01 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Surface Guild | docs/modules/scanner/surface | Create `/docs/modules/scanner/scanner-engine.md` covering Surface.FS/Env/Secrets workflow between Scanner, Zastava, Scheduler, and Ops. | Need latest surface emit notes (SCANNER-SURFACE-04) | DOSS0101 | | DOCS-SYMS-70-003 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · Symbols Guild | docs/modules/symbols | Author symbol-server architecture/spec docs (`docs/specs/symbols/SYMBOL_MANIFEST_v1.md`, API reference, bundle guide) and update reachability guides with symbol lookup workflow and tenant controls. Dependencies: SYMS-SERVER-401-011, SYMS-INGEST-401-013. | Need RBSY0101 cache notes | DOSY0101 | -| DOCS-TEN-47-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` outlining scope grammar, tenant model, imposed rule reminder. | Need tenancy ADR from DVDO0110 | DOTN0101 | -| DOCS-TEN-48-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md`. Dependencies: DOCS-TEN-47-001. | Depends on #1 | DOTN0101 | -| DOCS-TEN-49-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/tenancy | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, update `/docs/install/configuration-reference.md` with new env vars, all ending with imposed rule line. Dependencies: DOCS-TEN-48-001. | Requires monitoring plan from DVDO0110 | DOTN0101 | -| DOCS-TEST-62-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · SDK Generator Guild | docs/sdk | Author `/docs/testing/contract-testing.md` covering mock server, replay tests, golden fixtures. | Depends on #1 | DOSK0101 | -| DOCS-VEX-30-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-overview.md` describing purpose, scope, AOC guarantees. | Need PLVL0102 schema snapshot | DOVX0101 | -| DOCS-VEX-30-002 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Author `/docs/vex/consensus-algorithm.md` covering normalization, weighting, thresholds, examples. Dependencies: DOCS-VEX-30-001. | Depends on #1 | DOVX0101 | -| DOCS-VEX-30-003 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Issuer Directory Guild | docs/modules/vex-lens | Document `/docs/vex/issuer-directory.md` (issuer management, keys, trust overrides, audit). Dependencies: DOCS-VEX-30-002. | Requires Issuer Directory inputs | DOVX0101 | -| DOCS-VEX-30-004 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-api.md` with endpoint specs, query params, rate limits. Dependencies: DOCS-VEX-30-003. | Needs PLVL0102 policy join notes | DOVX0101 | -| DOCS-VEX-30-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Console Guild | docs/modules/vex-lens | Write `/docs/vex/consensus-console.md` covering UI workflows, filters, conflicts, accessibility. Dependencies: DOCS-VEX-30-004. | Requires console overlay assets | DOVX0101 | -| DOCS-VEX-30-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Policy Guild | docs/modules/vex-lens | Add `/docs/policy/vex-trust-model.md` detailing policy knobs, thresholds, simulation. Dependencies: DOCS-VEX-30-005. | Needs waiver/exception guidance | DOVX0101 | -| DOCS-VEX-30-007 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · SBOM Service Guild | docs/modules/vex-lens | Publish `/docs/sbom/vex-mapping.md` (CPE→purl strategy, edge cases, overrides). Dependencies: DOCS-VEX-30-006. | Depends on SBOM/VEX dataflow spec | DOVX0101 | -| DOCS-VEX-30-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/vex-lens | Deliver `/docs/security/vex-signatures.md` (verification flow, key rotation, audit). Dependencies: DOCS-VEX-30-007. | Requires security review (DVDO0110) | DOVX0101 | -| DOCS-VEX-30-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/vex-lens | Create `/docs/runbooks/vex-ops.md` for recompute storms, mapping failures, signature errors. Dependencies: DOCS-VEX-30-008. | Needs DevOps rollout plan | DOVX0101 | +| DOCS-TEN-47-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/security/tenancy-overview.md` and `/docs/security/scopes-and-roles.md` outlining scope grammar, tenant model, imposed rule reminder. | Need tenancy ADR from DVDO0110 | DOTN0101 | +| DOCS-TEN-48-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/tenancy | Publish `/docs/operations/multi-tenancy.md`, `/docs/operations/rls-and-data-isolation.md`, `/docs/console/admin-tenants.md`. Dependencies: DOCS-TEN-47-001. | Depends on #1 | DOTN0101 | +| DOCS-TEN-49-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/tenancy | Publish `/docs/modules/cli/guides/authentication.md`, `/docs/api/authentication.md`, `/docs/policy/examples/abac-overlays.md`, update `/docs/install/configuration-reference.md` with new env vars, all ending with imposed rule line. Dependencies: DOCS-TEN-48-001. | Requires monitoring plan from DVDO0110 | DOTN0101 | +| DOCS-TEST-62-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · SDK Generator Guild | docs/sdk | Author `/docs/testing/contract-testing.md` covering mock server, replay tests, golden fixtures. | Depends on #1 | DOSK0101 | +| DOCS-VEX-30-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-overview.md` describing purpose, scope, AOC guarantees. | Need PLVL0102 schema snapshot | DOVX0101 | +| DOCS-VEX-30-002 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Author `/docs/vex/consensus-algorithm.md` covering normalization, weighting, thresholds, examples. Dependencies: DOCS-VEX-30-001. | Depends on #1 | DOVX0101 | +| DOCS-VEX-30-003 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Issuer Directory Guild | docs/modules/vex-lens | Document `/docs/vex/issuer-directory.md` (issuer management, keys, trust overrides, audit). Dependencies: DOCS-VEX-30-002. | Requires Issuer Directory inputs | DOVX0101 | +| DOCS-VEX-30-004 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · VEX Lens Guild | docs/modules/vex-lens | Publish `/docs/vex/consensus-api.md` with endpoint specs, query params, rate limits. Dependencies: DOCS-VEX-30-003. | Needs PLVL0102 policy join notes | DOVX0101 | +| DOCS-VEX-30-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Console Guild | docs/modules/vex-lens | Write `/docs/vex/consensus-console.md` covering UI workflows, filters, conflicts, accessibility. Dependencies: DOCS-VEX-30-004. | Requires console overlay assets | DOVX0101 | +| DOCS-VEX-30-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Policy Guild | docs/modules/vex-lens | Add `/docs/policy/vex-trust-model.md` detailing policy knobs, thresholds, simulation. Dependencies: DOCS-VEX-30-005. | Needs waiver/exception guidance | DOVX0101 | +| DOCS-VEX-30-007 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · SBOM Service Guild | docs/modules/vex-lens | Publish `/docs/sbom/vex-mapping.md` (CPE→purl strategy, edge cases, overrides). Dependencies: DOCS-VEX-30-006. | Depends on SBOM/VEX dataflow spec | DOVX0101 | +| DOCS-VEX-30-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · Security Guild | docs/modules/vex-lens | Deliver `/docs/security/vex-signatures.md` (verification flow, key rotation, audit). Dependencies: DOCS-VEX-30-007. | Requires security review (DVDO0110) | DOVX0101 | +| DOCS-VEX-30-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild · DevOps Guild | docs/modules/vex-lens | Create `/docs/runbooks/vex-ops.md` for recompute storms, mapping failures, signature errors. Dependencies: DOCS-VEX-30-008. | Needs DevOps rollout plan | DOVX0101 | | DOCS-VEX-401-012 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · VEX Lens Guild | `docs/benchmarks/vex-evidence-playbook.md`, `bench/README.md` | Maintain the VEX Evidence Playbook, publish repo templates/README, and document verification workflows for operators. | Need VEX evidence export from PLVL0102 | DOVB0101 | -| DOCS-VULN-29-001 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-overview.md` covering domain model, identities, AOC guarantees, workflow summary. | Need GRAP0101 contract | DOVL0101 | -| DOCS-VULN-29-002 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Write `/docs/vuln/explorer-using-console.md` with workflows, screenshots, keyboard shortcuts, saved views, deep links. Dependencies: DOCS-VULN-29-001. | Depends on #1 | DOVL0101 | -| DOCS-VULN-29-003 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · UI Guild | docs/modules/vuln-explorer | Author `/docs/vuln/explorer-api.md` (endpoints, query schema, grouping, errors, rate limits). Dependencies: DOCS-VULN-29-002. | Requires UI assets | DOVL0101 | -| DOCS-VULN-29-004 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-cli.md` with command reference, samples, exit codes, CI snippets. Dependencies: DOCS-VULN-29-003. | Needs policy overlay inputs | DOVL0101 | -| DOCS-VULN-29-005 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Write `/docs/vuln/findings-ledger.md` detailing event schema, hashing, Merkle roots, replay tooling. Dependencies: DOCS-VULN-29-004. | Requires security review | DOVL0101 | -| DOCS-VULN-29-006 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Update `/docs/policy/vuln-determinations.md` for new rationale, signals, simulation semantics. Dependencies: DOCS-VULN-29-005. | Depends on DevOps rollout plan | DOVL0101 | -| DOCS-VULN-29-007 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Publish `/docs/vex/explorer-integration.md` covering CSAF mapping, suppression precedence, status semantics. Dependencies: DOCS-VULN-29-006. | Needs CLI examples (132_CLCI0110) | DOVL0101 | -| DOCS-VULN-29-008 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Export Center Guild | docs/modules/vuln-explorer | Publish `/docs/advisories/explorer-integration.md` covering key normalization, withdrawn handling, provenance. Dependencies: DOCS-VULN-29-007. | Need export bundle spec | DOVL0102 | -| DOCS-VULN-29-009 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Author `/docs/sbom/vuln-resolution.md` detailing version semantics, scope, paths, safe version hints. Dependencies: DOCS-VULN-29-008. | Depends on #1 | DOVL0102 | -| DOCS-VULN-29-010 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Publish `/docs/observability/vuln-telemetry.md` (metrics, logs, tracing, dashboards, SLOs). Dependencies: DOCS-VULN-29-009. | Requires DevOps automation plan | DOVL0102 | -| DOCS-VULN-29-011 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Notifications Guild | docs/modules/vuln-explorer | Create `/docs/security/vuln-rbac.md` for roles, ABAC policies, attachment encryption, CSRF. Dependencies: DOCS-VULN-29-010. | Needs notifications contract | DOVL0102 | -| DOCS-VULN-29-012 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Write `/docs/runbooks/vuln-ops.md` (projector lag, resolver storms, export failures, policy activation). Dependencies: DOCS-VULN-29-011. | Requires policy overlay outputs | DOVL0102 | -| DOCS-VULN-29-013 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API images, manifests, resource sizing, health checks. Dependencies: DOCS-VULN-29-012. | Needs CLI/export scripts from 132_CLCI0110 | DOVL0102 | +| DOCS-VULN-29-001 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-overview.md` covering domain model, identities, AOC guarantees, workflow summary. | Need GRAP0101 contract | DOVL0101 | +| DOCS-VULN-29-002 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Vuln Explorer Guild | docs/modules/vuln-explorer | Write `/docs/vuln/explorer-using-console.md` with workflows, screenshots, keyboard shortcuts, saved views, deep links. Dependencies: DOCS-VULN-29-001. | Depends on #1 | DOVL0101 | +| DOCS-VULN-29-003 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · UI Guild | docs/modules/vuln-explorer | Author `/docs/vuln/explorer-api.md` (endpoints, query schema, grouping, errors, rate limits). Dependencies: DOCS-VULN-29-002. | Requires UI assets | DOVL0101 | +| DOCS-VULN-29-004 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Publish `/docs/vuln/explorer-cli.md` with command reference, samples, exit codes, CI snippets. Dependencies: DOCS-VULN-29-003. | Needs policy overlay inputs | DOVL0101 | +| DOCS-VULN-29-005 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Write `/docs/vuln/findings-ledger.md` detailing event schema, hashing, Merkle roots, replay tooling. Dependencies: DOCS-VULN-29-004. | Requires security review | DOVL0101 | +| DOCS-VULN-29-006 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Update `/docs/policy/vuln-determinations.md` for new rationale, signals, simulation semantics. Dependencies: DOCS-VULN-29-005. | Depends on DevOps rollout plan | DOVL0101 | +| DOCS-VULN-29-007 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Publish `/docs/vex/explorer-integration.md` covering CSAF mapping, suppression precedence, status semantics. Dependencies: DOCS-VULN-29-006. | Needs CLI examples (132_CLCI0110) | DOVL0101 | +| DOCS-VULN-29-008 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Export Center Guild | docs/modules/vuln-explorer | Publish `/docs/advisories/explorer-integration.md` covering key normalization, withdrawn handling, provenance. Dependencies: DOCS-VULN-29-007. | Need export bundle spec | DOVL0102 | +| DOCS-VULN-29-009 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Security Guild | docs/modules/vuln-explorer | Author `/docs/sbom/vuln-resolution.md` detailing version semantics, scope, paths, safe version hints. Dependencies: DOCS-VULN-29-008. | Depends on #1 | DOVL0102 | +| DOCS-VULN-29-010 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevOps Guild | docs/modules/vuln-explorer | Publish `/docs/observability/vuln-telemetry.md` (metrics, logs, tracing, dashboards, SLOs). Dependencies: DOCS-VULN-29-009. | Requires DevOps automation plan | DOVL0102 | +| DOCS-VULN-29-011 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Notifications Guild | docs/modules/vuln-explorer | Create `/docs/security/vuln-rbac.md` for roles, ABAC policies, attachment encryption, CSRF. Dependencies: DOCS-VULN-29-010. | Needs notifications contract | DOVL0102 | +| DOCS-VULN-29-012 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · Policy Guild | docs/modules/vuln-explorer | Write `/docs/runbooks/vuln-ops.md` (projector lag, resolver storms, export failures, policy activation). Dependencies: DOCS-VULN-29-011. | Requires policy overlay outputs | DOVL0102 | +| DOCS-VULN-29-013 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild · DevEx/CLI Guild | docs/modules/vuln-explorer | Update `/docs/install/containers.md` with Findings Ledger & Vuln Explorer API images, manifests, resource sizing, health checks. Dependencies: DOCS-VULN-29-012. | Needs CLI/export scripts from 132_CLCI0110 | DOVL0102 | | DOWNLOADS-CONSOLE-23-001 | TODO | | SPRINT_0502_0001_0001_ops_deployment_ii | Docs Guild · Deployment Guild | docs/console | Maintain signed downloads manifest pipeline (images, Helm, offline bundles), publish JSON under `deploy/downloads/manifest.json`, and document sync cadence for Console + docs parity. | Need latest console build instructions | DOCN0101 | | DPOP-11-001 | TODO | 2025-11-08 | SPRINT_100_identity_signing | Docs Guild · Authority Core | src/Authority/StellaOps.Authority | Need DPoP ADR from PGMI0101 | AUTH-AOC-19-002 | DODP0101 | | DSL-401-005 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · Policy Guild | `docs/policy/dsl.md`, `docs/policy/lifecycle.md` | Depends on PLLG0101 DSL updates | Depends on PLLG0101 DSL updates | DODP0101 | @@ -3562,14 +3562,14 @@ | POLICY-27-004 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add lifecycle commands for publish/promote/rollback/sign with attestation checks. | Depends on 27-003 | | | POLICY-27-005 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild · Docs Guild | src/Cli/StellaOps.Cli | Update CLI refs/samples (JSON schemas, exit codes, CI snippets). | Requires 27-004 output | | | POLICY-27-006 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Update policy scopes/help text to request new Policy Studio scope family and adjust regression tests. | Needs 27-005 docs | | -| POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, DevEx/CLI Guild (docs) | | | | | -| POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | -| POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Security Guild (docs) | | | | | -| POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Architecture Guild (docs) | | | | | -| POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Observability Guild (docs) | | | | | -| POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Ops Guild (docs) | | | | | -| POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Policy Guild (docs) | | | | | -| POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_308_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | +| POLICY-27-007 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, DevEx/CLI Guild (docs) | | | | | +| POLICY-27-008 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | +| POLICY-27-009 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Security Guild (docs) | | | | | +| POLICY-27-010 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Architecture Guild (docs) | | | | | +| POLICY-27-011 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Observability Guild (docs) | | | | | +| POLICY-27-012 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Ops Guild (docs) | | | | | +| POLICY-27-013 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Policy Guild (docs) | | | | | +| POLICY-27-014 | BLOCKED | 2025-10-27 | SPRINT_0308_0001_0008_docs_tasks_md_viii | Docs Guild, Policy Registry Guild (docs) | | | | | | POLICY-401-026 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild · Concelier Guild (`docs/policy/dsl.md`, `docs/uncertainty/README.md`) | `docs/policy/dsl.md`, `docs/uncertainty/README.md` | | | | | POLICY-AIRGAP-56-001 | TODO | | SPRINT_123_policy_reasoning | Policy Guild | src/Policy/StellaOps.Policy.Engine | Support policy pack imports from Mirror Bundles, track `bundle_id` metadata, and ensure deterministic caching | Needs OFFK0101 bundle schema | | | POLICY-AIRGAP-56-002 | TODO | | SPRINT_123_policy_reasoning | Policy Guild · Policy Studio Guild | src/Policy/StellaOps.Policy.Engine | Export policy sub-bundles | POLICY-AIRGAP-56-001 | | @@ -3733,7 +3733,7 @@ | RISK-67-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | | RISK-67-002 | TODO | | SPRINT_128_policy_reasoning | Policy Guild / src/Policy/StellaOps.Policy.Engine | src/Policy/StellaOps.Policy.Engine | | POLICY-RISK-67-001 | | | RISK-67-003 | BLOCKED (2025-11-26) | | SPRINT_0128_0001_0001_policy_reasoning | Policy Guild, Risk Engine Guild / src/Policy/__Libraries/StellaOps.Policy | src/Policy/__Libraries/StellaOps.Policy | | POLICY-RISK-67-002 | Blocked by missing risk profile schema + lifecycle API contract. | -| RISK-67-004 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, CLI Guild (docs) | | | | | +| RISK-67-004 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, CLI Guild (docs) | | | | | | RISK-68-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild, Policy Studio Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | | RISK-68-002 | TODO | | SPRINT_128_policy_reasoning | Risk Profile Schema Guild / src/Policy/StellaOps.Policy.RiskProfile | src/Policy/StellaOps.Policy.RiskProfile | | POLICY-RISK-68-001 | | | RISK-69-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild, Notifications Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | @@ -3757,7 +3757,7 @@ | RISK-ENGINE-70-002 | TODO | | SPRINT_0129_0001_0001_policy_reasoning | Risk Engine Guild, Observability Guild / src/RiskEngine/StellaOps.RiskEngine | src/RiskEngine/StellaOps.RiskEngine | Integrate runtime evidence provider and reachability provider outputs with caching + TTL | RISK-ENGINE-70-001 | | | RULES-33-001 | REVIEW (2025-10-30) | 2025-10-30 | SPRINT_0506_0001_0001_ops_devops_iv | DevOps Guild, Platform Leads (ops/devops) | ops/devops | | | | | RUNBOOK-401-017 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Docs Guild · Ops Guild (`docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md`) | `docs/runbooks/reachability-runtime.md`, `docs/reachability/DELIVERY_GUIDE.md` | | | | -| RUNBOOK-55-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, Ops Guild (docs) | | | | | +| RUNBOOK-55-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, Ops Guild (docs) | | | | | | RUNBOOK-REPLAY-187-004 | TODO | | SPRINT_160_export_evidence | Docs/Ops Guild · `/docs/runbooks/replay_ops.md` | docs/runbooks/replay_ops.md | Docs/Ops Guild · `/docs/runbooks/replay_ops.md` | | | | RUNTIME-401-002 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Signals Guild (`src/Signals/StellaOps.Signals`) | `src/Signals/StellaOps.Signals` | | | | | RUNTIME-PROBE-401-010 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Runtime Signals Guild (`src/Signals/StellaOps.Signals.Runtime`, `ops/probes`) | `src/Signals/StellaOps.Signals.Runtime`, `ops/probes` | Implement lightweight runtime probes (EventPipe/.NET, JFR/JVM) that capture method enter events for the target components, package them as CAS traces, and feed them into the Signals ingestion pipeline. | | | @@ -3875,13 +3875,13 @@ | SCANNER-ANALYZERS-RUBY-28-010 | TODO | | SPRINT_135_scanner_surface | Ruby Analyzer Guild, Signals Guild (src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby) | src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby | Optional runtime evidence integration (if provided logs/metrics) with path hashing, without altering static precedence. | SCANNER-ANALYZERS-RUBY-28-009 | | | SCANNER-ANALYZERS-RUBY-28-011 | TODO | | SPRINT_135_scanner_surface | Ruby Analyzer Guild, DevOps Guild (src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby) | src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby | Package analyzer plug-in, add CLI (`stella ruby inspect`), refresh Offline Kit documentation. | SCANNER-ANALYZERS-RUBY-28-010 | | | SCANNER-ANALYZERS-RUBY-28-012 | TODO | | SPRINT_135_scanner_surface | Ruby Analyzer Guild (src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby) | src/Scanner/StellaOps.Scanner.Analyzers.Lang.Ruby | Policy signal emitter: rubygems drift, native extension flags, dangerous constructs counts, TLS verify posture, dynamic require eval warnings. | SCANNER-ANALYZERS-RUBY-28-011 | | -| SCANNER-BENCH-62-002 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | -| SCANNER-BENCH-62-003 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | -| SCANNER-BENCH-62-004 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Java Analyzer Guild (docs) | | | | | -| SCANNER-BENCH-62-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Go Analyzer Guild (docs) | | | | | -| SCANNER-BENCH-62-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Rust Analyzer Guild (docs) | | | | | -| SCANNER-BENCH-62-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, EntryTrace Guild (docs) | | | | | -| SCANNER-BENCH-62-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | | +| SCANNER-BENCH-62-002 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | +| SCANNER-BENCH-62-003 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Product Guild (docs) | | | | | +| SCANNER-BENCH-62-004 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Java Analyzer Guild (docs) | | | | | +| SCANNER-BENCH-62-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Go Analyzer Guild (docs) | | | | | +| SCANNER-BENCH-62-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Rust Analyzer Guild (docs) | | | | | +| SCANNER-BENCH-62-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, EntryTrace Guild (docs) | | | | | +| SCANNER-BENCH-62-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | | | SCANNER-CLI-0001 | DONE | 2025-11-10 | SPRINT_0138_0000_0001_scanner_ruby_parity | CLI Guild, Ruby Analyzer Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | Coordinate CLI UX/help text for new Ruby verbs and update CLI docs/golden outputs. | SCANNER-ENG-0019 | | | SCANNER-DET-01 | DONE (2025-12-03) | 2025-12-03 | SPRINT_0301_0001_0001_docs_md_i | Docs Guild · Scanner Guild | | Deterministic compose fixtures landed; docs published. | | | SCANNER-DOCS-0003 | TODO | | SPRINT_327_docs_modules_scanner | Docs Guild, Product Guild (docs/modules/scanner) | docs/modules/scanner | Gather Windows/macOS analyzer demand signals and record findings in `docs/benchmarks/scanner/windows-macos-demand.md` for marketing + product readiness. | | | @@ -3975,7 +3975,7 @@ | SDKREL-63-002 | TODO | | SPRINT_0208_0001_0001_sdk | SDK Release Guild, API Governance Guild (src/Sdk/StellaOps.Sdk.Release) | src/Sdk/StellaOps.Sdk.Release | Integrate changelog automation pulling from OAS diffs and generator metadata. Dependencies: SDKREL-63-001. | | | | SDKREL-64-001 | TODO | | SPRINT_0208_0001_0001_sdk | SDK Release Guild, Notifications Guild (src/Sdk/StellaOps.Sdk.Release) | src/Sdk/StellaOps.Sdk.Release | Hook SDK releases into Notifications Studio with scoped announcements and RSS/Atom feeds. Dependencies: SDKREL-63-002. | | | | SDKREL-64-002 | TODO | | SPRINT_0208_0001_0001_sdk | SDK Release Guild, Export Center Guild (src/Sdk/StellaOps.Sdk.Release) | src/Sdk/StellaOps.Sdk.Release | Add `devportal --offline` bundle job packaging docs, specs, SDK artifacts for air-gapped users. Dependencies: SDKREL-64-001. | | | -| SEC-62-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, Authority Core (docs) | | | | | +| SEC-62-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, Authority Core (docs) | | | | | | SEC-CRYPTO-90-001 | DONE | 2025-11-07 | SPRINT_514_sovereign_crypto_enablement | Security Guild (src/__Libraries/StellaOps.Cryptography) | src/__Libraries/StellaOps.Cryptography | Produce the RootPack_RU implementation plan, provider strategy (CryptoPro + PKCS#11), and backlog split for sovereign crypto work. | | | | SEC-CRYPTO-90-002 | DONE | 2025-11-07 | SPRINT_514_sovereign_crypto_enablement | Security Guild (src/__Libraries/StellaOps.Cryptography) | src/__Libraries/StellaOps.Cryptography | Extend signature/catalog constants and configuration schema to recognize `GOST12-256/512`, regional crypto profiles, and provider preference ordering. | | | | SEC-CRYPTO-90-003 | DONE | 2025-11-07 | SPRINT_514_sovereign_crypto_enablement | Security Guild (src/__Libraries/StellaOps.Cryptography) | src/__Libraries/StellaOps.Cryptography | Implement `StellaOps.Cryptography.Plugin.CryptoPro` provider (sign/verify/JWK export) using CryptoPro CSP with deterministic logging/tests. | | | @@ -3997,7 +3997,7 @@ | SEC-CRYPTO-90-019 | TODO | | SPRINT_514_sovereign_crypto_enablement | Security Guild | third_party/forks/AlexMAS.GostCryptography | Patch the fork to drop vulnerable `System.Security.Cryptography.{Pkcs,Xml}` 6.0.0 dependencies (target .NET 8+, adopt fixed BCL packages, re-run tests). | Needs fork validation | CRSA0101 | | SEC-CRYPTO-90-020 | TODO | | SPRINT_514_sovereign_crypto_enablement | Security Guild | src/__Libraries/StellaOps.Cryptography.Plugin.CryptoPro | Re-point `StellaOps.Cryptography.Plugin.CryptoPro` to the forked sources (replace NuGet package references, adjust DI wiring) and prove the plugin works end-to-end. | Depends on #5 | CRSA0101 | | SEC-CRYPTO-90-021 | TODO | | SPRINT_514_sovereign_crypto_enablement | Security + QA Guilds | scripts/crypto/**, docs/security/rootpack_ru_validation.md | Validate the forked library + plugin on both Windows (CryptoPro CSP) and Linux (OpenSSL GOST fallback) builds/tests; document any platform-specific prerequisites. | Depends on #6 | CRSA0101 | -| SEC-OBS-50-001 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, Security Guild (docs) | | | | | +| SEC-OBS-50-001 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, Security Guild (docs) | | | | | | SEC2 | DONE | 2025-11-09 | SPRINT_100_identity_signing | Security Guild, Storage Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard) | src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard | | | | | SEC3 | DONE | 2025-11-09 | SPRINT_100_identity_signing | Security Guild, BE-Auth Plugin (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard) | src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard | | | | | SEC5 | DONE | 2025-11-09 | SPRINT_100_identity_signing | Security Guild (src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard) | src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard | | | | @@ -4022,10 +4022,10 @@ | SIG-26-002 | TODO | | SPRINT_204_cli_iv | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | SIG-26-003 | TODO | | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/UI/StellaOps.UI) | src/UI/StellaOps.UI | | | | | SIG-26-004 | TODO | | SPRINT_0211_0001_0003_ui_iii | UI Guild (src/UI/StellaOps.UI) | src/UI/StellaOps.UI | | | | -| SIG-26-005 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, UI Guild (docs) | | | | | -| SIG-26-006 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, DevEx/CLI Guild (docs) | | | | | -| SIG-26-007 | TODO | | SPRINT_309_docs_tasks_md_ix | Docs Guild, BE-Base Platform Guild (docs) | | | | | -| SIG-26-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | | +| SIG-26-005 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, UI Guild (docs) | | | | | +| SIG-26-006 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, DevEx/CLI Guild (docs) | | | | | +| SIG-26-007 | TODO | | SPRINT_0309_0001_0009_docs_tasks_md_ix | Docs Guild, BE-Base Platform Guild (docs) | | | | | +| SIG-26-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | | | SIG-STORE-401-016 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Signals Guild · BE-Base Platform Guild (`src/Signals/StellaOps.Signals`, `src/__Libraries/StellaOps.Replay.Core`) | `src/Signals/StellaOps.Signals`, `src/__Libraries/StellaOps.Replay.Core` | Introduce shared reachability store collections (`func_nodes`, `call_edges`, `cve_func_hits`), indexes, and repository APIs so Scanner/Signals/Policy can reuse canonical function data. | | | | SIGN-CORE-186-004 | DONE | 2025-11-26 | SPRINT_186_record_deterministic_execution | Signing Guild | `src/Signer/StellaOps.Signer`, `src/__Libraries/StellaOps.Cryptography` | Replace the HMAC demo implementation in `StellaOps.Signer` with StellaOps.Cryptography providers (keyless + KMS), including provider selection, key material loading, and cosign-compatible DSSE signature output. | Mirrors #1 | SIGR0101 | | SIGN-CORE-186-005 | DONE | 2025-11-26 | SPRINT_186_record_deterministic_execution | Signing Guild | `src/Signer/StellaOps.Signer.Core` | Refactor `SignerStatementBuilder` to support StellaOps predicate types (e.g., `stella.ops/promotion@v1`) and delegate payload canonicalisation to the Provenance library once available. | Mirrors #2 | SIGR0101 | @@ -4161,7 +4161,7 @@ | TEN-48-001 | TODO | | SPRINT_115_concelier_iv | Concelier Core Guild (src/Concelier/__Libraries/StellaOps.Concelier.Core) | src/Concelier/__Libraries/StellaOps.Concelier.Core | | | | | TEN-49-001 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | TEST-186-006 | TODO | | SPRINT_186_record_deterministic_execution | Signing Guild, QA Guild (`src/Signer/StellaOps.Signer.Tests`) | `src/Signer/StellaOps.Signer.Tests` | | | | -| TEST-62-001 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Contract Testing Guild (docs) | | | | | +| TEST-62-001 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Contract Testing Guild (docs) | | | | | | TIME-57-001 | TODO | | SPRINT_0503_0001_0001_ops_devops_i | Exporter Guild · AirGap Time Guild · CLI Guild | | | PROGRAM-STAFF-1001 | | | TIME-57-002 | TODO | | SPRINT_510_airgap | Exporter Guild · AirGap Time Guild · CLI Guild | src/AirGap/StellaOps.AirGap.Time | PROGRAM-STAFF-1001 | PROGRAM-STAFF-1001 | AGTM0101 | | TIME-58-001 | TODO | | SPRINT_510_airgap | AirGap Time Guild | src/AirGap/StellaOps.AirGap.Time | AIRGAP-TIME-58-001 | AIRGAP-TIME-58-001 | AGTM0101 | @@ -4211,11 +4211,11 @@ | VEX-30-002 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | VEX-30-003 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | VEX-30-004 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | -| VEX-30-005 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Console Guild (docs) | | | | | -| VEX-30-006 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | DOVX0101 | +| VEX-30-005 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Console Guild (docs) | | | | | +| VEX-30-006 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Policy Guild (docs) | | | | DOVX0101 | | VEX-30-007 | TODO | | SPRINT_216_web_v | BE-Base Platform Guild, VEX Lens Guild (src/Web/StellaOps.Web) | src/Web/StellaOps.Web | | | DOVX0101 | -| VEX-30-008 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, Security Guild (docs) | | | | DOVX0101 | -| VEX-30-009 | TODO | | SPRINT_310_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | DOVX0101 | +| VEX-30-008 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, Security Guild (docs) | | | | DOVX0101 | +| VEX-30-009 | TODO | | SPRINT_0310_0001_0010_docs_tasks_md_x | Docs Guild, DevOps Guild (docs) | | | | DOVX0101 | | VEX-401-006 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild (`src/Policy/StellaOps.Policy.Engine`, `src/Policy/__Libraries/StellaOps.Policy`) | `src/Policy/StellaOps.Policy.Engine`, `src/Policy/__Libraries/StellaOps.Policy` | | | DOVX0101 | | VEX-401-010 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Policy Guild (`src/Policy/StellaOps.Policy.Engine/Vex`, `docs/modules/policy/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md`) | `src/Policy/StellaOps.Policy.Engine/Vex`, `docs/modules/policy/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md` | | | DOVX0101 | | VEX-401-011 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | verify | | | | DOVX0101 | @@ -4249,13 +4249,13 @@ | VULN-29-004 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild, Observability Guild (src/Concelier/StellaOps.Concelier.WebService) | src/Concelier/StellaOps.Concelier.WebService | | | | | VULN-29-005 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | | VULN-29-006 | TODO | | SPRINT_205_cli_v | DevEx/CLI Guild, Docs Guild (src/Cli/StellaOps.Cli) | src/Cli/StellaOps.Cli | | | | -| VULN-29-007 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Excititor Guild (docs) | | | | | -| VULN-29-008 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Concelier Guild (docs) | | | | | -| VULN-29-009 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, SBOM Service Guild (docs) | | | | | -| VULN-29-010 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Observability Guild (docs) | | | | | -| VULN-29-011 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Security Guild (docs) | | | | | -| VULN-29-012 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Ops Guild (docs) | | | | | -| VULN-29-013 | TODO | | SPRINT_311_docs_tasks_md_xi | Docs Guild, Deployment Guild (docs) | | | | | +| VULN-29-007 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Excititor Guild (docs) | | | | | +| VULN-29-008 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Concelier Guild (docs) | | | | | +| VULN-29-009 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, SBOM Service Guild (docs) | | | | | +| VULN-29-010 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Observability Guild (docs) | | | | | +| VULN-29-011 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Security Guild (docs) | | | | | +| VULN-29-012 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Ops Guild (docs) | | | | | +| VULN-29-013 | TODO | | SPRINT_0311_0001_0001_docs_tasks_md_xi | Docs Guild, Deployment Guild (docs) | | | | | | VULN-API-29-001 | DONE | 2025-11-25 | SPRINT_0129_0001_0001_policy_reasoning | Vuln Explorer API Guild / src/VulnExplorer/StellaOps.VulnExplorer.Api | src/VulnExplorer/StellaOps.VulnExplorer.Api | Define OpenAPI spec (list/detail/query/simulation/workflow/export), query JSON schema, pagination/grouping contracts, and error codes | | PLVA0101 | | VULN-API-29-002 | DONE | 2025-11-25 | SPRINT_0129_0001_0001_policy_reasoning | Vuln Explorer API Guild / src/VulnExplorer/StellaOps.VulnExplorer.Api | src/VulnExplorer/StellaOps.VulnExplorer.Api | Implement list/query endpoints with policy parameter, grouping, server paging, caching, and cost budgets; tests at `tests/TestResults/vuln-explorer/api.trx`. | VULN-API-29-001 | PLVA0101 | | VULN-API-29-003 | TODO | | SPRINT_0129_0001_0001_policy_reasoning | Vuln Explorer API Guild / src/VulnExplorer/StellaOps.VulnExplorer.Api | src/VulnExplorer/StellaOps.VulnExplorer.Api | Implement detail endpoint aggregating evidence, policy rationale, paths | VULN-API-29-002 | PLVA0101 | diff --git a/docs/modules/signals/dev-smoke/2025-12-05/SHA256SUMS b/docs/modules/signals/dev-smoke/2025-12-05/SHA256SUMS new file mode 100644 index 000000000..7be0cc386 --- /dev/null +++ b/docs/modules/signals/dev-smoke/2025-12-05/SHA256SUMS @@ -0,0 +1,6 @@ +f2e6a34b65d2c124c33fc79a7d8dadd29f3722a7c49f8af945460465326923e2 confidence_decay_config.sigstore.json +170892f6a48b0aef6f426ea97a86f6cd4420bc52634f12a92f72e20f0fa12e29 ../../decay/confidence_decay_config.yaml +ff87e5a97204ac4c0652bada480e7209027f66ab769cbcba230750e9023f9d16 unknowns_scoring_manifest.sigstore.json +450675035928e4771cca1b9e5f9e42035dbe10b3de7b66a4077a7b729b2c5b13 ../../unknowns/unknowns_scoring_manifest.json +b2c8b0a58a3e67b3872355a834fc03909372cd2fa371d29792260477e696a3ec heuristics_catalog.sigstore.json +e33fa0963493252a5ac379a12f820f6b356ea94310afd1db9ad7394e8307000e ../../heuristics/heuristics.catalog.json diff --git a/docs/modules/signals/dev-smoke/2025-12-05/confidence_decay_config.sigstore.json b/docs/modules/signals/dev-smoke/2025-12-05/confidence_decay_config.sigstore.json new file mode 100644 index 000000000..3444dcf95 --- /dev/null +++ b/docs/modules/signals/dev-smoke/2025-12-05/confidence_decay_config.sigstore.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"1/nAsWLsk/yOPl4sjynn6FOCC1ixnrbxSK9UHxjF8MQ="},"tlogEntries":[{"logIndex":"742648248","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1764914883","inclusionPromise":{"signedEntryTimestamp":"MEQCIEPXbH67jXKh8Yq1qT5s1CSnxFasd3pQeXPSn/+qyxYBAiAfMJC+eKMcBihxab9b6TfflHj04sNXv771AqjVLfZgqg=="},"inclusionProof":{"logIndex":"620743986","rootHash":"qIy16SiTCMU2N72AhLBlJx4tpzQ8wuRLgz7c0jPwZbg=","treeSize":"620743987","hashes":["bnLgpW9yyZyVLYG614bzegWryTevnj6R4THhcUw9xA8=","rwvg8t8pLgSLqXTX3SYw+yaTB5IBVFAeay6sg4iZTeE=","eaWE2OLV03T+OyLNcVSyZQNJl7KX0NVvVOP9hn9wKhg=","pN3WsrqWd5COntsHSMxViShmEptV4D7TJb4GHTu48gU=","j8r8ZbYGLFTKmzzPG6Rx/Nfbpnb8lXciUmjSRfmxw54=","t5cbUDhG60F85rSNrRV+TIjBaDyMw/Q7BygwBC6RmMI=","2oggqwKSybajJsAPYRL+lhzR8rg5UnNrX7SPVNnhiko=","xzp7CAFO8oL+EOUFxA7Uvwu36mxnTidQVpK4flWaiQo=","v+EjDtrntRCwx7q5IKS6Vl8rpAnSDht87Hsyx/lqYEE=","jz8AosZUL+zxO0wYDWJ/XEXmGsHXcAB4SJkTREB+Y4o=","j0S9eqN7H3FvIfEcZzdPYNUncd4169EpD2ouhdWszEw=","OwMBv2+d/917ew5VN1ZtUAPzljoADlvS+mBOPRX2lYU=","Mo/+V8ftGFQQbS+XsKdaF+l1sDADl3NB/NC1OoAr9WM=","RsQ5xuBa0gKvWk53V8F8JismpQAqEf9N2nqMjFfr/KA=","etMFukD8mHOD37ceTwB1Al2nC3iIzy/CTtNjwflJmDE=","huaH1ZSkRyP4+vpmGtpmkkL845lhcmN9io8MIe6Sob0=","ZmUkYkHBy1B723JrEgiKvepTdHYrP6y2a4oODYvi5VY=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n620743987\nqIy16SiTCMU2N72AhLBlJx4tpzQ8wuRLgz7c0jPwZbg=\n\n— rekor.sigstore.dev wNI9ajBFAiEAxYkz+TT4nAzW4mkpci9k2BUNhK5iAVWG36AfTtniDMACIC7oFZsGsNsXWEkqxOPhpCcxLnVYURN9NMfpqHMKeM9i\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIxNzA4OTJmNmE0OGIwYWVmNmY0MjZlYTk3YTg2ZjZjZDQ0MjBiYzUyNjM0ZjEyYTkyZjcyZTIwZjBmYTEyZTI5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJREo4cTh1VG1PK2FlS2lpSVU1VWhwWXlvNTArazM5TkMzenptdmo5TDZDWUFpRUEreWpyK1hhaHExUG51QXpqbHBJVUpRajVhaFhrS0RKS1lZYVpqYlBZL3dBPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWm05Skt6bFNSa05VWTJacVpVMXhjRU5STTBaQmVYWkxkMEpSVlFwWlFVbE5NbU5tUkZJNFZ6azRUM2h1V0ZZcloyWldOVVJvWm05cE9IRnZaa0Z1Unk5MlF6ZEVZa0pzV0RKMEwyZFVOMGRMVlZwQlEyaEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgdSW4YGrlOdy3eV6JIVvUfQRDbvrf/0k2S9/ga8HRl4ACFQDh5WrELclf0K61HErdp+xAS1LzOBgPMjAyNTEyMDUwNjA4MDNaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MTIwNTA2MDgwM1owLwYJKoZIhvcNAQkEMSIEIAvhBOIQRprk7w8+NkmwCiBeRT6XyqQuq7caFieLHnzIMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMEGcCpDWnYn1cWV0DfX/+k+u3jgiJupYStrmO6xnSwffS2hAbYXb9WHtZhk+eC6iXwIxAKXJ8nd4jkjLjsfTOS3JyyJpNiRJMUIjLaZGM1xTgxVYZvh7spUgLFwqwQL0763oHw=="}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"FwiS9qSLCu9vQm6peob2zUQgvFJjTxKpL3LiDw+hLik="},"signature":"MEUCIDJ8q8uTmO+aeKiiIU5UhpYyo50+k39NC3zzmvj9L6CYAiEA+yjr+Xahq1PnuAzjlpIUJQj5ahXkKDJKYYaZjbPY/wA="}} \ No newline at end of file diff --git a/docs/modules/signals/dev-smoke/2025-12-05/heuristics_catalog.sigstore.json b/docs/modules/signals/dev-smoke/2025-12-05/heuristics_catalog.sigstore.json new file mode 100644 index 000000000..b548e85a1 --- /dev/null +++ b/docs/modules/signals/dev-smoke/2025-12-05/heuristics_catalog.sigstore.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"1/nAsWLsk/yOPl4sjynn6FOCC1ixnrbxSK9UHxjF8MQ="},"tlogEntries":[{"logIndex":"742648276","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1764914886","inclusionPromise":{"signedEntryTimestamp":"MEYCIQCv9TES7atMeHgnXgj6/4o6p5E3f9czATR1Sf/RgD8oEwIhANoqNtNZNtEiY2GJsQaEV2MjO1b3BesOLHrebytZM+AP"},"inclusionProof":{"logIndex":"620744014","rootHash":"Nd8odlYH1sZUqtwWLZd87SAZ3aDZE9/kvN8KP+WYJ4U=","treeSize":"620744015","hashes":["gSbu849KPGEFvvB7ZtGv4iiUJsQdIui3dBp3UXTjLeE=","gxllKTeg511WcKk8smE+D8AB/kc/I1KEpR+8znuM+WY=","0ktQJInLByqOQpv6DDn23DjwzrKQ4rQdxLnvK4mwU4s=","uWcfGHF7BrVZ0akB9sqpBCARjJ7zRFCaiBpaHFD2TE8=","pN3WsrqWd5COntsHSMxViShmEptV4D7TJb4GHTu48gU=","j8r8ZbYGLFTKmzzPG6Rx/Nfbpnb8lXciUmjSRfmxw54=","t5cbUDhG60F85rSNrRV+TIjBaDyMw/Q7BygwBC6RmMI=","2oggqwKSybajJsAPYRL+lhzR8rg5UnNrX7SPVNnhiko=","xzp7CAFO8oL+EOUFxA7Uvwu36mxnTidQVpK4flWaiQo=","v+EjDtrntRCwx7q5IKS6Vl8rpAnSDht87Hsyx/lqYEE=","jz8AosZUL+zxO0wYDWJ/XEXmGsHXcAB4SJkTREB+Y4o=","j0S9eqN7H3FvIfEcZzdPYNUncd4169EpD2ouhdWszEw=","OwMBv2+d/917ew5VN1ZtUAPzljoADlvS+mBOPRX2lYU=","Mo/+V8ftGFQQbS+XsKdaF+l1sDADl3NB/NC1OoAr9WM=","RsQ5xuBa0gKvWk53V8F8JismpQAqEf9N2nqMjFfr/KA=","etMFukD8mHOD37ceTwB1Al2nC3iIzy/CTtNjwflJmDE=","huaH1ZSkRyP4+vpmGtpmkkL845lhcmN9io8MIe6Sob0=","ZmUkYkHBy1B723JrEgiKvepTdHYrP6y2a4oODYvi5VY=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n620744015\nNd8odlYH1sZUqtwWLZd87SAZ3aDZE9/kvN8KP+WYJ4U=\n\n— rekor.sigstore.dev wNI9ajBFAiEA37CfHOQAhbL30a3zqMuGfOPCMdaN7H2tjwUpXVSpNGMCIHqoJjARFDGTZGf7qZdY8o/GSFdCNVzSCJ2B5EeeoG0w\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlMzNmYTA5NjM0OTMyNTJhNWFjMzc5YTEyZjgyMGY2YjM1NmVhOTQzMTBhZmQxZGI5YWQ3Mzk0ZTgzMDcwMDBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQnp0YjZFT1JJT29rdXlzOU9kOTBibmVyQWNyRTRDWVlVaHFubk9jL2FqY0FpQVUvd2JnU0VHNjAvbjhMUVU5enFZWUo1aFB1UitMOFQ5dmFCdTd2UTZQTWc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWm05Skt6bFNSa05VWTJacVpVMXhjRU5STTBaQmVYWkxkMEpSVlFwWlFVbE5NbU5tUkZJNFZ6azRUM2h1V0ZZcloyWldOVVJvWm05cE9IRnZaa0Z1Unk5MlF6ZEVZa0pzV0RKMEwyZFVOMGRMVlZwQlEyaEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgdlN9Oq58PexreDhAoALSZcxSEz/yH4qlvFpjBd68YFsCFQDAeuqgvxWOAsR8SvxXP6PFXVYxXRgPMjAyNTEyMDUwNjA4MDZaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MTIwNTA2MDgwNlowLwYJKoZIhvcNAQkEMSIEIJymRlFuL4lrrsGhDJhlU1XKcXXboDkys6DNqUDDbCIHMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMQD7xPDVXYmjOxIIiMR+QVE/80Euhl9Ze03qY4INSaJRJRclTdwZrluP5uV9V2L8+HkCMF+kt+40LnsKYwd3DUtbc6YQGMxl0LOPqv/p6BoW07V0C5aEDX2AZ5F/kiVfdupgwQ=="}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"4z+gljSTJSpaw3mhL4IPazVuqUMQr9Hbmtc5ToMHAA4="},"signature":"MEQCIBztb6EORIOokuys9Od90bnerAcrE4CYYUhqnnOc/ajcAiAU/wbgSEG60/n8LQU9zqYYJ5hPuR+L8T9vaBu7vQ6PMg=="}} \ No newline at end of file diff --git a/docs/modules/signals/dev-smoke/2025-12-05/unknowns_scoring_manifest.sigstore.json b/docs/modules/signals/dev-smoke/2025-12-05/unknowns_scoring_manifest.sigstore.json new file mode 100644 index 000000000..77360d558 --- /dev/null +++ b/docs/modules/signals/dev-smoke/2025-12-05/unknowns_scoring_manifest.sigstore.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"1/nAsWLsk/yOPl4sjynn6FOCC1ixnrbxSK9UHxjF8MQ="},"tlogEntries":[{"logIndex":"742648261","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1764914885","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDMUtyUhnBrQY/KiBy0qf9twT0oL7UfA7JE+tZEfIx5PQIhAIzGkn0n0wRRUyLUrIsFfjhYxD/YsQ+0FbFJVAewheW2"},"inclusionProof":{"logIndex":"620743999","rootHash":"RlwXlMPk2OTu0Q0hH3A5mFxv0EP88REQq8A7N7AOgKk=","treeSize":"620744000","hashes":["eKAQSo78J8w9/CQGM3bznJOCAlob/hAQ52qBV876JeM=","9rT7Nu1z1+n5lu0RnB3fQ9tTO0oE8eURjQd3x6Y2QbM=","xLWKkyqh1mMRlsFwxAVuzyvyrhuihA8hoaVNIHznXE8=","1GPmVbjRGJ4j4iMQUYzEmn5/E9RluOw3B9RsAmyw2aA=","rwvg8t8pLgSLqXTX3SYw+yaTB5IBVFAeay6sg4iZTeE=","eaWE2OLV03T+OyLNcVSyZQNJl7KX0NVvVOP9hn9wKhg=","pN3WsrqWd5COntsHSMxViShmEptV4D7TJb4GHTu48gU=","j8r8ZbYGLFTKmzzPG6Rx/Nfbpnb8lXciUmjSRfmxw54=","t5cbUDhG60F85rSNrRV+TIjBaDyMw/Q7BygwBC6RmMI=","2oggqwKSybajJsAPYRL+lhzR8rg5UnNrX7SPVNnhiko=","xzp7CAFO8oL+EOUFxA7Uvwu36mxnTidQVpK4flWaiQo=","v+EjDtrntRCwx7q5IKS6Vl8rpAnSDht87Hsyx/lqYEE=","jz8AosZUL+zxO0wYDWJ/XEXmGsHXcAB4SJkTREB+Y4o=","j0S9eqN7H3FvIfEcZzdPYNUncd4169EpD2ouhdWszEw=","OwMBv2+d/917ew5VN1ZtUAPzljoADlvS+mBOPRX2lYU=","Mo/+V8ftGFQQbS+XsKdaF+l1sDADl3NB/NC1OoAr9WM=","RsQ5xuBa0gKvWk53V8F8JismpQAqEf9N2nqMjFfr/KA=","etMFukD8mHOD37ceTwB1Al2nC3iIzy/CTtNjwflJmDE=","huaH1ZSkRyP4+vpmGtpmkkL845lhcmN9io8MIe6Sob0=","ZmUkYkHBy1B723JrEgiKvepTdHYrP6y2a4oODYvi5VY=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n620744000\nRlwXlMPk2OTu0Q0hH3A5mFxv0EP88REQq8A7N7AOgKk=\n\n— rekor.sigstore.dev wNI9ajBEAiA9169mYGzChioOq4yrn6u+U8/ZN8GMMnt2xbByB7WL0wIgZOm7F6Q1SLvvGViw66NXmRcfw+9vR5LtGmPGIIgq4iY=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0NTA2NzUwMzU5MjhlNDc3MWNjYTFiOWU1ZjllNDIwMzVkYmUxMGIzZGU3YjY2YTQwNzdhN2I3MjliMmM1YjEzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSEYyaTNJY0JBQ041UlVMQms2SHgvNHRZOWZtV2FtbEZzekwzazhPbzZjbEFpQWhSNWxkaEtYdFlTMVZsYXB2OVNJMlhOeHFtQVpsVjlDeTNLVWdFcUdqM0E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWm05Skt6bFNSa05VWTJacVpVMXhjRU5STTBaQmVYWkxkMEpSVlFwWlFVbE5NbU5tUkZJNFZ6azRUM2h1V0ZZcloyWldOVVJvWm05cE9IRnZaa0Z1Unk5MlF6ZEVZa0pzV0RKMEwyZFVOMGRMVlZwQlEyaEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg5DHI7cUk4rMoHBpTEYnzvCpNRrwG0QjgRFKlYjSn+8ACFQDLbZWEIC18EVB82MiYMd888cpePBgPMjAyNTEyMDUwNjA4MDVaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MTIwNTA2MDgwNVowLwYJKoZIhvcNAQkEMSIEIHTBAC/E7/3aJKF4E7bMV6cRH58G6BMmQZ+K2QrgxlNjMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMGsQv8SpoW8xPUgIvFVuQ2rYBg6SN/jZdE+Ebe46hKUVI9E/agtmvIfYaa8B2XeBPgIxANpfyZx/dhAVWx4SVx4cion/AEDKAaOTfz15bDq3yrJWY1i4Cm78ww2AsLIuDpJiMQ=="}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"RQZ1A1ko5HccyhueX55CA12+ELPee2akB3p7cpssWxM="},"signature":"MEQCIHF2i3IcBACN5RULBk6Hx/4tY9fmWamlFszL3k8Oo6clAiAhR5ldhKXtYS1Vlapv9SI2XNxqmAZlV9Cy3KUgEqGj3A=="}} \ No newline at end of file diff --git a/docs/modules/signals/evidence/README.md b/docs/modules/signals/evidence/README.md index 8f05fc401..def612ca9 100644 --- a/docs/modules/signals/evidence/README.md +++ b/docs/modules/signals/evidence/README.md @@ -29,22 +29,22 @@ Signed artifacts uploaded as workflow artifact `signals-dsse-signed-{run}` and o ## Development Signing (Local Testing) -A development key pair is available for smoke tests: +A development key pair is available for smoke tests. Recent dev bundles live under `docs/modules/signals/dev-smoke/2025-12-04/` and `docs/modules/signals/dev-smoke/2025-12-05/`. ```bash # Sign with dev key COSIGN_ALLOW_DEV_KEY=1 COSIGN_PASSWORD=stellaops-dev \ - OUT_DIR=docs/modules/signals/dev-test \ + OUT_DIR=docs/modules/signals/dev-smoke/2025-12-05 \ tools/cosign/sign-signals.sh # Verify signature cosign verify-blob \ --key tools/cosign/cosign.dev.pub \ - --bundle docs/modules/signals/dev-test/confidence_decay_config.sigstore.json \ + --bundle docs/modules/signals/dev-smoke/2025-12-05/confidence_decay_config.sigstore.json \ docs/modules/signals/decay/confidence_decay_config.yaml ``` -**Note**: Dev key signatures are NOT suitable for Evidence Locker or production use. +**Note**: Dev key signatures are NOT suitable for Evidence Locker or production use; tlog upload is disabled. ## Production Signing (Manual) diff --git a/docs/modules/taskrunner/architecture.md b/docs/modules/taskrunner/architecture.md index 7f2d75df8..7281fb4fa 100644 --- a/docs/modules/taskrunner/architecture.md +++ b/docs/modules/taskrunner/architecture.md @@ -82,7 +82,19 @@ } ``` -## 12. References +## 12. Gap Remediation (TP1–TP10, 2025-12) +- **Canonical plan hash (TP1):** Plan hash is `sha256` over `plan.canonicalPlanPath` (normalized JSON, stable key ordering, UTF-8). Hash and canonical plan file are shipped in offline bundles and verified by `scripts/packs/verify_offline_bundle.py`. +- **Inputs lock (TP2):** Task Runner emits `inputs.lock` capturing resolved inputs + redacted secret placeholders; stored in evidence bundles and listed under `hashes[]` in offline manifests. +- **Approval ledger (TP3):** Approval decisions are DSSE-signed, embedding `runId`, `gateId`, `planHash`, and `tenantId`. Approval endpoints reject mismatched plan hashes or missing DSSE envelopes. +- **Secret redaction (TP4):** Evidence/transcripts apply the redaction policy referenced in `security.secretsRedactionPolicy`; secrets are hashed or blanked, never logged in clear text. +- **Deterministic ordering/RNG/time (TP5):** Execution order derives from the canonical graph, RNG seed is derived from `planHash`, and all timestamps are UTC ISO-8601 with monotonic log sequences. +- **Sandbox + egress quotas (TP6):** Runs declare `sandbox.mode` (`sealed`/`restricted`), explicit `egressAllowlist`, CPU/memory limits, and optional wall-clock quota. Missing entries cause fail-closed refusal during plan or execution. +- **Registry signing + SBOM + revocation (TP7):** Packs accepted by Task Runner must include DSSE envelopes for bundle + attestation, a pack SBOM, and a revocation list path; imports fail when digests or revocation proofs are absent. +- **Offline bundle schema + verifier (TP8):** Offline bundles must satisfy `docs/task-packs/packs-offline-bundle.schema.json` and pass `scripts/packs/verify_offline_bundle.py --require-dsse`. Evidence locker records the verifier version used. +- **Run/approval SLOs (TP9):** Plan validation enforces declared SLOs (`runP95Seconds`, `approvalP95Seconds`, `maxQueueDepth`) and wires alert rules into telemetry (burn-rate alerts on approval latency + queue depth). +- **Fail-closed gates (TP10):** Approval/policy/timeline gates default to fail-closed on missing evidence, expired DSSE, or absent quotas; remediation hints surface in `pack_run_logs` and API error payloads. + +## 13. References - Product advisory: `docs/product-advisories/29-Nov-2025 - Task Pack Orchestration and Automation.md`. - Task Pack spec + authoring + runbook: `docs/task-packs/spec.md`, `docs/task-packs/authoring-guide.md`, `docs/task-packs/runbook.md`. - Migration detail: `docs/modules/taskrunner/migrations/pack-run-collections.md`. diff --git a/docs/product-advisories/31-Nov-2025 FINDINGS.md b/docs/product-advisories/31-Nov-2025 FINDINGS.md index 9bcee9ec8..26c8d49d1 100644 --- a/docs/product-advisories/31-Nov-2025 FINDINGS.md +++ b/docs/product-advisories/31-Nov-2025 FINDINGS.md @@ -92,10 +92,22 @@ This advisory consolidates late-November gap findings across Scanner, SBOM/VEX s 9. **NR9 — Offline notify-kit with DSSE**: Produce offline kit containing schemas, rules/templates, connector configs, verify script, and DSSE-signed manifest; include hash list and time-anchor hook; support deterministic packaging flags and tenant/env scoping. Evidence: kit manifest + DSSE + `verify_notify_kit.sh` script. 10. **NR10 — Mandatory simulations & evidence**: Rules/templates must pass simulation/dry-run against frozen fixtures before activation; store DSSE-signed simulation results and attach evidence to change approvals; require regression tests for each high-impact rule change. Evidence: simulation report + DSSE + golden fixtures and TRX/NDJSON outputs. +## TP (Task Pack) Gaps — TP1–TP10 +1. **TP1 — Canonical schemas + plan-hash recipe**: Freeze pack manifest canonicalization (sorted JSON, UTF-8, no insignificant whitespace) and compute `plan.hash` as `sha256` over `plan.canonicalPlanPath`. Evidence: `docs/task-packs/packs-offline-bundle.schema.json`, fixtures hashed by `scripts/packs/verify_offline_bundle.py`. +2. **TP2 — Inputs lock evidence**: Every pack run must emit `inputs.lock` containing resolved inputs, secret placeholders, and digests; stored and hashed in offline bundle `hashes[]`. Evidence: offline bundle manifest + deterministic hash list. +3. **TP3 — Approval RBAC/DSSE records**: Approval decisions are recorded as DSSE ledgers (`evidence.approvalsLedger`) with Authority claims `pack_run_id`, `pack_gate_id`, `pack_plan_hash`, and tenant context; Task Runner rejects approvals lacking matching plan hash. Evidence: approvals DSSE + ledger hash. +4. **TP4 — Secret redaction policy**: Bundle includes `security.secretsRedactionPolicy` describing hashing/redaction of secrets; transcripts and evidence bundles store only redacted forms. Evidence: policy doc referenced in bundle manifest + redaction fixtures. +5. **TP5 — Deterministic ordering/RNG/time**: Execution order, RNG seed (`plan.rngSeed` derived from plan hash), and timestamps (UTC ISO-8601) are fixed; logs are strictly sequenced. Evidence: canonical plan + deterministic log fixtures. +6. **TP6 — Sandbox/egress limits + quotas**: Offline bundle declares sandbox mode (`sealed`/`restricted`), explicit `egressAllowlist`, CPU/memory quotas, and optional `quotaSeconds`; Task Runner fails if absent. Evidence: sandbox block in manifest + enforcement tests. +7. **TP7 — Pack registry signing + SBOM + revocation**: Registry entries ship DSSE envelopes for bundle + attestation, pack SBOM path (`pack.sbom`), and a revocation list path (`security.revocations`) enforced during import. Evidence: registry record with SBOM digest + revocation list referenced in manifest. +8. **TP8 — Offline pack-bundle schema + verify script**: Offline bundles must conform to `packs-offline-bundle.schema.json` and pass `scripts/packs/verify_offline_bundle.py --bundle --require-dsse`. Evidence: successful verify run + manifest hash list. +9. **TP9 — Run/approval SLOs + alerting**: Bundle declares SLOs (`slo.runP95Seconds`, `slo.approvalP95Seconds`, `slo.maxQueueDepth`) with alert rules referenced in `slo.alertRules`; observability must surface breaches. Evidence: alert rule file + metrics fixtures. +10. **TP10 — Gate fail-closed defaults**: Approval/policy/timeline gates default to fail-closed when evidence, DSSE, or quotas are missing/expired; Task Runner aborts with remediation hint. Evidence: negative-path fixtures showing fail-closed behavior. + ## Pending Families (to be expanded) The following gap families were referenced in November indices and still need detailed findings written out: -- CV1–CV10 (CVSS v4 receipts), CVM1–CVM10 (momentum), FC1–FC10 (SCA fixture gaps), OB1–OB10 (onboarding), IG1–IG10 (implementor guidance), RR1–RR10 (Rekor receipts), SK1–SK10 (standups), MI1–MI10 (UI micro-interactions), PVX1–PVX10 (Proof-linked VEX UI), TTE1–TTE10 (Time-to-Evidence), AR-EP1…AR-VB1 (archived advisories revival), BP1–BP10 (SBOM→VEX proof pipeline), UT1–UT10 (unknown heuristics), CE1–CE10 (evidence patterns), ET1–ET10 (ecosystem fixtures), RB1–RB10 (reachability fixtures), G1–G12 / RD1–RD10 (reachability benchmark/dataset), UN1–UN10 (unknowns registry), U1–U10 (decay), EX1–EX10 (explainability), VEX1–VEX10 (VEX claims), BR1–BR10 (binary reachability), VT1–VT10 (triage), PL1–PL10 (plugin arch), EB1–EB10 (evidence baseline), EC1–EC10 (export center), AT1–AT10 (automation), OK1–OK10 / RK1–RK10 / MS1–MS10 (offline/mirror/Rekor kits), TP1–TP10 (task packs), AU1–AU10 (auth), CL1–CL10 (CLI), OR1–OR10 (orchestrator), ZR1–ZR10 (Zastava), NR1–NR10 (Notify), GA1–GA10 (graph analytics), TO1–TO10 (telemetry), PS1–PS10 (policy), FL1–FL10 (ledger), CI1–CI10 (Concelier ingest). - - CV1–CV10 (CVSS v4 receipts), CVM1–CVM10 (momentum), FC1–FC10 (SCA fixture gaps), OB1–OB10 (onboarding), IG1–IG10 (implementor guidance), RR1–RR10 (Rekor receipts), SK1–SK10 (standups), MI1–MI10 (UI micro-interactions), PVX1–PVX10 (Proof-linked VEX UI), TTE1–TTE10 (Time-to-Evidence), AR-EP1…AR-VB1 (archived advisories revival), BP1–BP10 (SBOM→VEX proof pipeline), UT1–UT10 (unknown heuristics), CE1–CE10 (evidence patterns), ET1–ET10 (ecosystem fixtures), RB1–RB10 (reachability fixtures), G1–G12 / RD1–RD10 (reachability benchmark/dataset), UN1–UN10 (unknowns registry), U1–U10 (decay), EX1–EX10 (explainability), VEX1–VEX10 (VEX claims), BR1–BR10 (binary reachability), VT1–VT10 (triage), PL1–PL10 (plugin arch), EB1–EB10 (evidence baseline), EC1–EC10 (export center), AT1–AT10 (automation), OK1–OK10 / RK1–RK10 / MS1–MS10 (offline/mirror/Rekor kits), TP1–TP10 (task packs), AU1–AU10 (auth), CL1–CL10 (CLI), OR1–OR10 (orchestrator), ZR1–ZR10 (Zastava), GA1–GA10 (graph analytics), TO1–TO10 (telemetry), PS1–PS10 (policy), FL1–FL10 (ledger), CI1–CI10 (Concelier ingest). +- CV1–CV10 (CVSS v4 receipts), CVM1–CVM10 (momentum), FC1–FC10 (SCA fixture gaps), OB1–OB10 (onboarding), IG1–IG10 (implementor guidance), RR1–RR10 (Rekor receipts), SK1–SK10 (standups), MI1–MI10 (UI micro-interactions), PVX1–PVX10 (Proof-linked VEX UI), TTE1–TTE10 (Time-to-Evidence), AR-EP1…AR-VB1 (archived advisories revival), BP1–BP10 (SBOM→VEX proof pipeline), UT1–UT10 (unknown heuristics), CE1–CE10 (evidence patterns), ET1–ET10 (ecosystem fixtures), RB1–RB10 (reachability fixtures), G1–G12 / RD1–RD10 (reachability benchmark/dataset), UN1–UN10 (unknowns registry), U1–U10 (decay), EX1–EX10 (explainability), VEX1–VEX10 (VEX claims), BR1–BR10 (binary reachability), VT1–VT10 (triage), PL1–PL10 (plugin arch), EB1–EB10 (evidence baseline), EC1–EC10 (export center), AT1–AT10 (automation), OK1–OK10 / RK1–RK10 / MS1–MS10 (offline/mirror/Rekor kits), AU1–AU10 (auth), CL1–CL10 (CLI), OR1–OR10 (orchestrator), ZR1–ZR10 (Zastava), NR1–NR10 (Notify), GA1–GA10 (graph analytics), TO1–TO10 (telemetry), PS1–PS10 (policy), FL1–FL10 (ledger), CI1–CI10 (Concelier ingest). + - CV1–CV10 (CVSS v4 receipts), CVM1–CVM10 (momentum), FC1–FC10 (SCA fixture gaps), OB1–OB10 (onboarding), IG1–IG10 (implementor guidance), RR1–RR10 (Rekor receipts), SK1–SK10 (standups), MI1–MI10 (UI micro-interactions), PVX1–PVX10 (Proof-linked VEX UI), TTE1–TTE10 (Time-to-Evidence), AR-EP1…AR-VB1 (archived advisories revival), BP1–BP10 (SBOM→VEX proof pipeline), UT1–UT10 (unknown heuristics), CE1–CE10 (evidence patterns), ET1–ET10 (ecosystem fixtures), RB1–RB10 (reachability fixtures), G1–G12 / RD1–RD10 (reachability benchmark/dataset), UN1–UN10 (unknowns registry), U1–U10 (decay), EX1–EX10 (explainability), VEX1–VEX10 (VEX claims), BR1–BR10 (binary reachability), VT1–VT10 (triage), PL1–PL10 (plugin arch), EB1–EB10 (evidence baseline), EC1–EC10 (export center), AT1–AT10 (automation), OK1–OK10 / RK1–RK10 / MS1–MS10 (offline/mirror/Rekor kits), AU1–AU10 (auth), CL1–CL10 (CLI), OR1–OR10 (orchestrator), ZR1–ZR10 (Zastava), GA1–GA10 (graph analytics), TO1–TO10 (telemetry), PS1–PS10 (policy), FL1–FL10 (ledger), CI1–CI10 (Concelier ingest). Each pending family should be expanded in this document (or split into dedicated, linked supplements) with numbered findings, recommended evidence, and deterministic test/fixture expectations. diff --git a/docs/product-advisories/ADVISORY_INDEX.md b/docs/product-advisories/ADVISORY_INDEX.md index 327efc2ad..76ea19c72 100644 --- a/docs/product-advisories/ADVISORY_INDEX.md +++ b/docs/product-advisories/ADVISORY_INDEX.md @@ -24,16 +24,16 @@ These are the authoritative advisories to reference for implementation: ### SCA Failure Catalogue - **Canonical:** `29-Nov-2025 - SCA Failure Catalogue for StellaOps Tests.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/29-Nov-2025 - SCA Failure Catalogue for StellaOps Tests.md` (this catalogue) - - `docs/implplan/SPRINT_300_documentation_process.md` (tracking sync) + - `docs/implplan/SPRINT_0300_0001_0001_documentation_process.md` (tracking sync) - **Gaps:** `31-Nov-2025 FINDINGS.md` (FC1–FC10 remediation task SCA-FIXTURE-GAPS-300-014) - **Status:** Captures five real-world regressions/ SBOM gaps for Trivy/Syft/Grype/Snyk and frames test vectors + alarm scenarios for StellaOps acceptance suites. ### Mid-Level .NET Onboarding (Quick Start) - **Canonical:** `29-Nov-2025 - StellaOps – Mid-Level .NET Onboarding (Quick Start).md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/onboarding/dev-quickstart.md` (to be updated) - `docs/modules/platform/architecture-overview.md` @@ -42,7 +42,7 @@ These are the authoritative advisories to reference for implementation: ### Implementor Guidelines - **Canonical:** `30-Nov-2025 - Implementor Guidelines for Stella Ops.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/30-Nov-2025 - Implementor Guidelines for Stella Ops.md` (this briefing) - `docs/05_SYSTEM_REQUIREMENTS_SPEC.md` / `docs/13_RELEASE_ENGINEERING_PLAYBOOK.md` (reference requirements) @@ -58,7 +58,7 @@ These are the authoritative advisories to reference for implementation: ### Standup Sprint Kickstarters - **Canonical:** `30-Nov-2025 - Standup Sprint Kickstarters.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** `docs/implplan/README.md` (sprint template) - **Gaps:** `31-Nov-2025 FINDINGS.md` (SK1–SK10 remediation task STANDUP-GAPS-300-019) - **Status:** Introduces ceremony primer but lacks template alignment, readiness evidence, dependency ledger, offline/async guidance, metrics/SLOs, and role/decision capture rules. @@ -86,14 +86,14 @@ These are the authoritative advisories to reference for implementation: ### Archived Advisories (15–23 Nov 2025) - **Canonical:** `docs/product-advisories/archived/*.md` (embedded provenance events, function-level VEX explainability, binary reachability branches, SBOM-provenance spine, etc.) -- **Sprint:** SPRINT_300_documentation_process.md (triage/decision) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (triage/decision) - **Related Docs:** None current (need revival + canonicalization) - **Gaps:** `31-Nov-2025 FINDINGS.md` (AR-EP1 … AR-VB1 remediation task ARCHIVED-GAPS-300-020) - **Status:** Archived set lacks schemas, determinism rules, redaction/licensing, changelog/signing, and duplication resolution; needs triage on which to revive into active advisories. ### SBOM → VEX Proof Blueprint - **Canonical:** `29-Nov-2025 - SBOM to VEX Proof Pipeline Blueprint.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/29-Nov-2025 - SBOM to VEX Proof Pipeline Blueprint.md` (itself) - `docs/modules/platform/architecture-overview.md` (platform dossier link) @@ -102,7 +102,7 @@ These are the authoritative advisories to reference for implementation: ### UI Micro-Interactions - **Canonical:** `30-Nov-2025 - UI Micro-Interactions for StellaOps.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `apps/console/src/app/shared/micro/` - `docs/product-advisories/30-Nov-2025 - UI Micro-Interactions for StellaOps.md` @@ -125,7 +125,7 @@ These are the authoritative advisories to reference for implementation: ### Ecosystem Reality Tests - **Canonical:** `30-Nov-2025 - Ecosystem Reality Test Cases for StellaOps.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/30-Nov-2025 - Ecosystem Reality Test Cases for StellaOps.md` - **Status:** Evidence-backed acceptance tests covering credential leaks, offline DB quirks, SBOM parity, and scanner instability. @@ -140,14 +140,14 @@ These are the authoritative advisories to reference for implementation: ### Standup Sprint Kickstarters - **Canonical:** `30-Nov-2025 - Standup Sprint Kickstarters.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/30-Nov-2025 - Standup Sprint Kickstarters.md` - **Status:** Three day-0 tasks (scanner regressions, Postgres slice, DSSE/Rekor sweep) with ticket names and assignments. ### Evidence + Suppression Patterns - **Canonical:** `30-Nov-2025 - Comparative Evidence Patterns for Stella Ops.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/30-Nov-2025 - Comparative Evidence Patterns for Stella Ops.md` - **Gaps:** `31-Nov-2025 FINDINGS.md` (CE1–CE10 remediation task EVIDENCE-PATTERNS-GAPS-300-016) @@ -155,7 +155,7 @@ These are the authoritative advisories to reference for implementation: ### Ecosystem Reality Test Cases - **Canonical:** `30-Nov-2025 - Ecosystem Reality Test Cases.md` -- **Sprint:** SPRINT_300_documentation_process.md (docs tracker) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (docs tracker) - **Related Docs:** - `docs/product-advisories/30-Nov-2025 - Ecosystem Reality Test Cases.md` - **Gaps:** `31-Nov-2025 FINDINGS.md` (ET1–ET10 remediation task ECOSYS-FIXTURES-GAPS-300-017) @@ -309,10 +309,10 @@ These are the authoritative advisories to reference for implementation: - **Status:** Export profiles/adapters; determinism, provenance, and offline kit parity need gap remediation. ### Acceptance Tests Pack for Guardrails - **Canonical:** `29-Nov-2025 - Acceptance Tests Pack for StellaOps Guardrails.md` -- **Sprint:** SPRINT_300_documentation_process.md (Docs Governance) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (Docs Governance) - **Related Docs:** - `docs/product-advisories/29-Nov-2025 - Acceptance Tests Pack for StellaOps Guardrails.md` (itself) - - `docs/implplan/SPRINT_300_documentation_process.md` (tracking the sync) + - `docs/implplan/SPRINT_0300_0001_0001_documentation_process.md` (tracking the sync) - **Gaps:** `31-Nov-2025 FINDINGS.md` (AT1–AT10 remediation task AT-GAPS-300-012) - **Status:** Captures feed resiliency, SBOM validation, snapshot/replay rehearsals, reachability fallbacks, and pipeline swap guardrails for acceptance tests. @@ -456,7 +456,7 @@ These are the authoritative advisories to reference for implementation: - **Sprint:** SPRINT_0186_0001_0001_record_deterministic_execution.md (PRIMARY) - **Related Sprints:** - SPRINT_0120_0000_0001_policy_reasoning.md - - SPRINT_311_docs_tasks_md_xi.md + - SPRINT_0311_0001_0001_docs_tasks_md_xi.md - **Related Docs:** - `docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml` - OpenAPI spec - **Gaps:** `31-Nov-2025 FINDINGS.md` (FL1–FL10 remediation task LEDGER-GAPS-121-009) @@ -590,7 +590,7 @@ For each topic, the implementer should read: ### Developer Onboarding Quick Start - **Canonical:** `29-Nov-2025 - StellaOps – Mid-Level .NET Onboarding (Quick Start).md` -- **Sprint:** SPRINT_300_documentation_process.md (Docs Governance) +- **Sprint:** SPRINT_0300_0001_0001_documentation_process.md (Docs Governance) - **Related Docs:** - `docs/onboarding/dev-quickstart.md` (derived from this advisory) - `docs/README.md` (new quickstart reference) diff --git a/docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Findings Ledger and Immutable Audit Trail.md b/docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Findings Ledger and Immutable Audit Trail.md index 6f04611a0..5515171ff 100644 --- a/docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Findings Ledger and Immutable Audit Trail.md +++ b/docs/product-advisories/archived/27-Nov-2025-superseded/28-Nov-2025 - Findings Ledger and Immutable Audit Trail.md @@ -381,7 +381,7 @@ airgap: - **Primary Sprint:** SPRINT_0186_0001_0001_record_deterministic_execution.md - **Related Sprints:** - SPRINT_0120_0000_0001_policy_reasoning.md - - SPRINT_311_docs_tasks_md_xi.md + - SPRINT_0311_0001_0001_docs_tasks_md_xi.md **Key Task IDs:** - `LEDGER-CORE-40-001` - Event store (DONE) diff --git a/docs/router/SPRINT_7000_0011_0001_router_testing.md b/docs/router/SPRINT_7000_0011_0001_router_testing.md new file mode 100644 index 000000000..e20c1458e --- /dev/null +++ b/docs/router/SPRINT_7000_0011_0001_router_testing.md @@ -0,0 +1,92 @@ +# Sprint 7000-0011-0001 - Router Testing Sprint + +## Topic & Scope + +Create comprehensive test coverage for StellaOps Router projects. **Critical gap**: `StellaOps.Router.Transport.RabbitMq` has **NO tests**. + +**Goal:** ~192 tests covering all Router components with shared testing infrastructure. + +**Working directory:** `src/__Libraries/__Tests/` + +## Dependencies & Concurrency + +- **Upstream:** All Router libraries at stable v1.0 state (sprints 7000-0001 through 7000-0010) +- **Downstream:** None. Testing sprint. +- **Parallel work:** TST-001 through TST-004 can run in parallel. +- **Cross-module impact:** None. Tests only. + +## Documentation Prerequisites + +- `docs/router/specs.md` (complete specification) +- `docs/router/implplan.md` (phase guidance) +- Existing test patterns in `src/__Libraries/__Tests/StellaOps.Router.Transport.Tcp.Tests/` + +> **BLOCKED Tasks:** Before working on BLOCKED tasks, review [../implplan/BLOCKED_DEPENDENCY_TREE.md](../implplan/BLOCKED_DEPENDENCY_TREE.md) for root blockers and dependencies. + +## Delivery Tracker + +| # | Task ID | Status | Priority | Description | Notes | +|---|---------|--------|----------|-------------|-------| +| 1 | TST-001 | TODO | High | Create shared testing infrastructure (`StellaOps.Router.Testing`) | Enables all other tasks | +| 2 | TST-002 | TODO | Critical | Create RabbitMq transport test project skeleton | Critical gap | +| 3 | TST-003 | TODO | High | Implement Router.Common tests | FrameConverter, PathMatcher | +| 4 | TST-004 | TODO | High | Implement Router.Config tests | validation, hot-reload | +| 5 | TST-005 | TODO | Critical | Implement RabbitMq transport unit tests | ~35 tests | +| 6 | TST-006 | TODO | Medium | Expand Microservice SDK tests | EndpointRegistry, RequestDispatcher | +| 7 | TST-007 | TODO | Medium | Expand Transport.InMemory tests | Concurrency scenarios | +| 8 | TST-008 | TODO | Medium | Create integration test suite | End-to-end flows | +| 9 | TST-009 | TODO | Low | Expand TCP/TLS transport tests | Edge cases | +| 10 | TST-010 | TODO | Low | Create SourceGen integration tests | Optional | + +## Current State + +| Project | Test Location | Status | +|---------|--------------|--------| +| Router.Common | `tests/StellaOps.Router.Common.Tests` | Exists (skeletal) | +| Router.Config | `tests/StellaOps.Router.Config.Tests` | Exists (skeletal) | +| Router.Transport.InMemory | `tests/StellaOps.Router.Transport.InMemory.Tests` | Exists (skeletal) | +| Router.Transport.Tcp | `src/__Libraries/__Tests/` | Exists | +| Router.Transport.Tls | `src/__Libraries/__Tests/` | Exists | +| Router.Transport.Udp | `tests/StellaOps.Router.Transport.Udp.Tests` | Exists (skeletal) | +| **Router.Transport.RabbitMq** | **NONE** | **MISSING** | +| Microservice | `tests/StellaOps.Microservice.Tests` | Exists | +| Microservice.SourceGen | N/A | Source generator | + +## Test Counts Summary + +| Component | Unit | Integration | Total | +|-----------|------|-------------|-------| +| Router.Common | 35 | 0 | 35 | +| Router.Config | 25 | 3 | 28 | +| **Transport.RabbitMq** | **30** | **5** | **35** | +| Microservice SDK | 28 | 5 | 33 | +| Transport.InMemory | 23 | 5 | 28 | +| Integration Suite | 0 | 15 | 15 | +| TCP/TLS Expansion | 12 | 0 | 12 | +| SourceGen | 0 | 6 | 6 | +| **TOTAL** | **153** | **39** | **~192** | + +## Exit Criteria + +Before marking this sprint DONE: +1. [ ] All test projects compile +2. [ ] RabbitMq transport has comprehensive unit tests (critical gap closed) +3. [ ] Router.Common coverage > 90% for FrameConverter, PathMatcher +4. [ ] Router.Config coverage > 85% for RouterConfigProvider +5. [ ] All tests follow AAA pattern with comments +6. [ ] Integration tests demonstrate end-to-end flows +7. [ ] All tests added to CI/CD workflow + +## Execution Log + +| Date (UTC) | Update | Owner | +|------------|--------|-------| +| | | | + +## Decisions & Risks + +- All new test projects in `src/__Libraries/__Tests/` following existing pattern +- RabbitMQ unit tests use mocked interfaces (no real broker required) +- Integration tests may use Testcontainers for real broker testing +- xUnit v3 with FluentAssertions 6.12.0 +- Test naming: `[Method]_[Scenario]_[Expected]` diff --git a/docs/schemas/object-storage.schema.json b/docs/schemas/object-storage.schema.json new file mode 100644 index 000000000..e2ac5c911 --- /dev/null +++ b/docs/schemas/object-storage.schema.json @@ -0,0 +1,279 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stellaops.org/schemas/object-storage.schema.json", + "title": "StellaOps Object Storage Contract", + "description": "Contract for S3-compatible object storage used by Concelier for large raw payloads. Defines the interface for deterministic pointers, provenance metadata, and migration from GridFS.", + "version": "1.0.0", + "definitions": { + "ObjectPointer": { + "type": "object", + "description": "Deterministic pointer to an object in storage", + "required": ["bucket", "key", "sha256", "size"], + "properties": { + "bucket": { + "type": "string", + "description": "S3 bucket name (tenant-prefixed)", + "pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$" + }, + "key": { + "type": "string", + "description": "Object key (deterministic, content-addressed)", + "pattern": "^[a-zA-Z0-9/._-]+$" + }, + "sha256": { + "type": "string", + "description": "SHA-256 hash of object content (hex encoded)", + "pattern": "^[a-f0-9]{64}$" + }, + "size": { + "type": "integer", + "description": "Object size in bytes", + "minimum": 0 + }, + "contentType": { + "type": "string", + "description": "MIME type of the object", + "default": "application/octet-stream" + }, + "encoding": { + "type": "string", + "description": "Content encoding if compressed", + "enum": ["identity", "gzip", "zstd"] + } + } + }, + "ProvenanceMetadata": { + "type": "object", + "description": "Provenance metadata preserved from original ingestion", + "required": ["sourceId", "ingestedAt", "tenantId"], + "properties": { + "sourceId": { + "type": "string", + "description": "Identifier of the original data source", + "format": "uri" + }, + "ingestedAt": { + "type": "string", + "description": "UTC ISO-8601 timestamp of original ingestion", + "format": "date-time" + }, + "tenantId": { + "type": "string", + "description": "Tenant identifier for multi-tenant isolation", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "originalFormat": { + "type": "string", + "description": "Original format before normalization", + "enum": ["json", "xml", "csv", "ndjson", "yaml"] + }, + "originalSize": { + "type": "integer", + "description": "Original size before any transformation", + "minimum": 0 + }, + "transformations": { + "type": "array", + "description": "List of transformations applied", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["compression", "normalization", "redaction", "migration"] + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "agent": { + "type": "string", + "description": "Agent/service that performed transformation" + } + } + } + }, + "gridFsLegacyId": { + "type": "string", + "description": "Original GridFS ObjectId for migration tracking", + "pattern": "^[a-f0-9]{24}$" + } + } + }, + "StorageConfig": { + "type": "object", + "description": "Configuration for S3-compatible object storage endpoint", + "required": ["endpoint", "region"], + "properties": { + "endpoint": { + "type": "string", + "description": "S3-compatible endpoint URL (MinIO, AWS S3, etc.)", + "format": "uri" + }, + "region": { + "type": "string", + "description": "Storage region (use 'us-east-1' for MinIO)", + "default": "us-east-1" + }, + "usePathStyle": { + "type": "boolean", + "description": "Use path-style addressing (required for MinIO)", + "default": true + }, + "bucketPrefix": { + "type": "string", + "description": "Prefix for tenant bucket names", + "default": "stellaops-" + }, + "maxObjectSize": { + "type": "integer", + "description": "Maximum object size in bytes (default 5GB)", + "default": 5368709120 + }, + "compressionThreshold": { + "type": "integer", + "description": "Objects larger than this (bytes) will be compressed", + "default": 1048576 + } + } + }, + "MigrationRecord": { + "type": "object", + "description": "Record of a migration from GridFS to S3", + "required": ["gridFsId", "pointer", "migratedAt", "status"], + "properties": { + "gridFsId": { + "type": "string", + "description": "Original GridFS ObjectId", + "pattern": "^[a-f0-9]{24}$" + }, + "pointer": { + "$ref": "#/definitions/ObjectPointer" + }, + "migratedAt": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "enum": ["pending", "migrated", "verified", "failed", "tombstoned"] + }, + "verifiedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when content hash was verified post-migration" + }, + "rollbackAvailable": { + "type": "boolean", + "description": "Whether GridFS tombstone still exists for rollback", + "default": true + } + } + }, + "PayloadReference": { + "type": "object", + "description": "Reference to a large payload stored in object storage (used in advisory_observations)", + "required": ["type", "pointer", "provenance"], + "properties": { + "type": { + "type": "string", + "const": "object-storage-ref", + "description": "Discriminator for payload type" + }, + "pointer": { + "$ref": "#/definitions/ObjectPointer" + }, + "provenance": { + "$ref": "#/definitions/ProvenanceMetadata" + }, + "inline": { + "type": "boolean", + "description": "If true, payload is small enough to be inline (not in object storage)", + "default": false + }, + "inlineData": { + "type": "string", + "description": "Base64-encoded inline data (only if inline=true and size < threshold)", + "contentEncoding": "base64" + } + } + } + }, + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/StorageConfig" + }, + "pointers": { + "type": "array", + "items": { + "$ref": "#/definitions/PayloadReference" + } + }, + "migrations": { + "type": "array", + "items": { + "$ref": "#/definitions/MigrationRecord" + } + } + }, + "examples": [ + { + "config": { + "endpoint": "http://minio.stellaops.local:9000", + "region": "us-east-1", + "usePathStyle": true, + "bucketPrefix": "stellaops-", + "compressionThreshold": 1048576 + }, + "pointers": [ + { + "type": "object-storage-ref", + "pointer": { + "bucket": "stellaops-tenant-abc123", + "key": "advisories/raw/2025/12/05/sha256-a1b2c3d4.json.zst", + "sha256": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "size": 524288, + "contentType": "application/json", + "encoding": "zstd" + }, + "provenance": { + "sourceId": "https://nvd.nist.gov/feeds/json/cve/1.1", + "ingestedAt": "2025-12-05T10:30:00Z", + "tenantId": "tenant-abc123", + "originalFormat": "json", + "originalSize": 2097152, + "transformations": [ + { + "type": "compression", + "timestamp": "2025-12-05T10:30:01Z", + "agent": "concelier-ingest-v1.2.0" + } + ] + }, + "inline": false + } + ], + "migrations": [ + { + "gridFsId": "507f1f77bcf86cd799439011", + "pointer": { + "bucket": "stellaops-tenant-abc123", + "key": "advisories/migrated/507f1f77bcf86cd799439011.json", + "sha256": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", + "size": 102400, + "contentType": "application/json", + "encoding": "identity" + }, + "migratedAt": "2025-12-05T11:00:00Z", + "status": "verified", + "verifiedAt": "2025-12-05T11:00:05Z", + "rollbackAvailable": true + } + ] + } + ] +} diff --git a/docs/task-packs/packs-offline-bundle.schema.json b/docs/task-packs/packs-offline-bundle.schema.json new file mode 100644 index 000000000..c892dd68f --- /dev/null +++ b/docs/task-packs/packs-offline-bundle.schema.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "StellaOps Task Pack Offline Bundle", + "description": "Canonical offline bundle manifest for Task Packs; used by verify_offline_bundle.py and TaskRunner evidence checks.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "pack", + "plan", + "evidence", + "security", + "hashes", + "slo", + "tenant", + "environment", + "created" + ], + "properties": { + "schemaVersion": { + "type": "string", + "const": "stellaops.pack.offline-bundle.v1" + }, + "pack": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version", "bundle", "digest", "sbom"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "minLength": 1 }, + "bundle": { "type": "string", "description": "Relative path to the pack bundle tarball or OCI layout." }, + "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "registry": { "type": "string", "description": "Logical registry identifier or OCI reference." }, + "sbom": { "type": "string", "description": "Relative path to CycloneDX/SBOM document for the pack bundle." } + } + }, + "plan": { + "type": "object", + "additionalProperties": false, + "required": ["hashAlgorithm", "hash", "canonicalPlanPath", "inputsLock"], + "properties": { + "hashAlgorithm": { "type": "string", "enum": ["sha256"] }, + "hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "canonicalPlanPath": { "type": "string", "description": "Normalized JSON plan used to compute plan hash." }, + "inputsLock": { "type": "string", "description": "Deterministic lock of resolved inputs/secrets (hashed, redacted)." }, + "rngSeed": { "type": "string", "description": "Seed derived from plan hash for deterministic RNG." }, + "timestampSource": { "type": "string", "enum": ["utc-iso8601"], "description": "Time source requirement." } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["attestation", "approvalsLedger"], + "properties": { + "attestation": { "type": "string", "description": "DSSE payload binding run to plan hash." }, + "approvalsLedger": { "type": "string", "description": "DSSE-signed approvals ledger with Authority claims." }, + "timeline": { "type": "string", "description": "Optional timeline NDJSON for steps/policy events." } + } + }, + "security": { + "type": "object", + "additionalProperties": false, + "required": ["sandbox", "revocations", "signatures", "secretsRedactionPolicy"], + "properties": { + "sandbox": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "egressAllowlist", "cpuLimitMillicores", "memoryLimitMiB"], + "properties": { + "mode": { "type": "string", "enum": ["sealed", "restricted"] }, + "egressAllowlist": { + "type": "array", + "items": { "type": "string" } + }, + "cpuLimitMillicores": { "type": "integer", "minimum": 1 }, + "memoryLimitMiB": { "type": "integer", "minimum": 1 }, + "quotaSeconds": { "type": "integer", "minimum": 1 } + } + }, + "revocations": { "type": "string", "description": "Revocation list for pack versions/digests." }, + "signatures": { + "type": "object", + "additionalProperties": false, + "required": ["bundleDsse", "attestationDsse"], + "properties": { + "bundleDsse": { "type": "string" }, + "attestationDsse": { "type": "string" }, + "registryCertChain": { "type": "string" } + } + }, + "secretsRedactionPolicy": { "type": "string", "description": "Policy document describing hashing/redaction of secrets." } + } + }, + "hashes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "algorithm", "digest"], + "properties": { + "path": { "type": "string" }, + "algorithm": { "type": "string", "enum": ["sha256"] }, + "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } + } + } + }, + "slo": { + "type": "object", + "additionalProperties": false, + "required": ["runP95Seconds", "approvalP95Seconds", "maxQueueDepth"], + "properties": { + "runP95Seconds": { "type": "integer", "minimum": 1 }, + "approvalP95Seconds": { "type": "integer", "minimum": 1 }, + "maxQueueDepth": { "type": "integer", "minimum": 1 }, + "alertRules": { "type": "string", "description": "Path to alert rule definitions." } + } + }, + "tenant": { "type": "string", "minLength": 1 }, + "environment": { "type": "string", "minLength": 1 }, + "created": { "type": "string", "format": "date-time" }, + "expires": { "type": "string", "format": "date-time" }, + "verifyScriptVersion": { "type": "string", "description": "Version of verify_offline_bundle.py used to validate this bundle." } + } +} diff --git a/docs/task-packs/registry.md b/docs/task-packs/registry.md index d35f08547..0f602382a 100644 --- a/docs/task-packs/registry.md +++ b/docs/task-packs/registry.md @@ -168,7 +168,16 @@ Extensions must be deterministic and derived from signed bundle data. - [ ] Operations guidance covers backups, rotation, disaster recovery. - [ ] Imposed rule reminder included at top of document. +## 11 · TP Gap Remediation (2025-12) + +- **Signed registry record (TP7):** Every pack version stores DSSE envelopes for bundle + attestation, SBOM path, and revocation list reference. Imports fail-closed when signatures or revocation proofs are missing. +- **Offline bundle schema (TP8):** Registry exports offline artefacts that must satisfy `docs/task-packs/packs-offline-bundle.schema.json`; publish pipeline invokes `scripts/packs/verify_offline_bundle.py --require-dsse` before promotion. +- **Hash ledger (TP1/TP2):** Publish step writes `hashes[]` (sha256) for manifest, canonical plan, `inputs.lock`, approvals ledger, SBOM, and revocations; digests surface in audit events and `digestmap.json`. +- **Sandbox + quotas (TP6):** Registry metadata carries `sandbox.mode`, explicit egress allowlists, CPU/memory limits, and quota seconds; Task Runner refuses packs missing these fields. +- **SLO + alerting (TP9):** Pack metadata includes SLOs (`runP95Seconds`, `approvalP95Seconds`, `maxQueueDepth`); registry emits metrics/alerts when declared SLOs are exceeded during publish/import flows. +- **Fail-closed imports (TP10):** Import/mirror paths abort when DSSE, hash entries, or revocation files are absent or stale, returning actionable error codes for CLI/Task Runner. + --- -*Last updated: 2025-10-27 (Sprint 43).* +*Last updated: 2025-12-05 (Sprint 0157-0001-0001 TaskRunner I).* diff --git a/docs/task-packs/spec.md b/docs/task-packs/spec.md index 3b3d947a0..bcff2f841 100644 --- a/docs/task-packs/spec.md +++ b/docs/task-packs/spec.md @@ -167,6 +167,18 @@ pack.yaml ──▶ schema validation ──▶ expression audit ──▶ deter Packs must pass CLI validation before publishing. +### 6.1 · TP Gap Remediation (2025-12) +- **Canonical plan hash (TP1):** Compute `plan.hash` as `sha256` over canonical JSON (`plan.canonicalPlanPath`) with sorted keys and normalized numbers/booleans. The canonical plan file ships in offline bundles. +- **Inputs lock (TP2):** CLI emits `inputs.lock` capturing resolved inputs and redacted secret placeholders; hashed via `hashes[]` and included in evidence bundles. +- **Approval ledger DSSE (TP3):** Approval responses are DSSE-signed ledgers embedding `runId`, `gateId`, `planHash`, and tenant context; Task Runner rejects approvals without matching plan hash. +- **Secret redaction (TP4):** `security.secretsRedactionPolicy` defines hashing/redaction for secrets and PII; transcripts/evidence must reference this policy. +- **Deterministic RNG/time (TP5):** RNG seed is derived from `plan.hash`; timestamps use UTC ISO-8601; log ordering is monotonic. +- **Sandbox + egress quotas (TP6):** Packs declare `sandbox.mode`, explicit `egressAllowlist`, CPU/memory limits, and optional `quotaSeconds`; missing fields cause fail-closed refusal. +- **Registry signing + revocation (TP7):** Bundles carry SBOM + DSSE envelopes and reference a revocation list enforced during registry import. +- **Offline bundle schema + verifier (TP8):** Offline exports must satisfy `docs/task-packs/packs-offline-bundle.schema.json` and pass `scripts/packs/verify_offline_bundle.py --require-dsse`. +- **SLO + alerting (TP9):** Manifests declare `slo.runP95Seconds`, `slo.approvalP95Seconds`, `slo.maxQueueDepth`, and optional `slo.alertRules`; telemetry enforces and alerts on breaches. +- **Fail-closed gates (TP10):** Approval/policy/timeline gates fail closed when DSSE, hash entries, or quotas are missing/expired; CLI surfaces remediation hints. + --- ## 7 · Signatures & Provenance @@ -245,4 +257,4 @@ CLI enforces compatibility: running pack with unsupported features yields `ERR_P --- -*Last updated: 2025-10-27 (Sprint 43).* +*Last updated: 2025-12-05 (Sprint 0157-0001-0001 TaskRunner I).* diff --git a/evidence-locker/signals/2025-12-05/SHA256SUMS b/evidence-locker/signals/2025-12-05/SHA256SUMS new file mode 100644 index 000000000..668c3f4e7 --- /dev/null +++ b/evidence-locker/signals/2025-12-05/SHA256SUMS @@ -0,0 +1,6 @@ +33e4d8628cb81e8c724e16b0284cdc55148b74807a46398fd380a04460befa14 confidence_decay_config.sigstore.json +170892f6a48b0aef6f426ea97a86f6cd4420bc52634f12a92f72e20f0fa12e29 ../../../docs/modules/signals/decay/confidence_decay_config.yaml +1556236e853c09c0195003ab5d24aeb0622d6409bcd733eacf90d2248a9095fb unknowns_scoring_manifest.sigstore.json +450675035928e4771cca1b9e5f9e42035dbe10b3de7b66a4077a7b729b2c5b13 ../../../docs/modules/signals/unknowns/unknowns_scoring_manifest.json +8997764d15b1497bddd6b902432d8d80913bc792656e9c71750e669a1bee03ae heuristics_catalog.sigstore.json +e33fa0963493252a5ac379a12f820f6b356ea94310afd1db9ad7394e8307000e ../../../docs/modules/signals/heuristics/heuristics.catalog.json diff --git a/evidence-locker/signals/2025-12-05/confidence_decay_config.sigstore.json b/evidence-locker/signals/2025-12-05/confidence_decay_config.sigstore.json new file mode 100644 index 000000000..788a0a257 --- /dev/null +++ b/evidence-locker/signals/2025-12-05/confidence_decay_config.sigstore.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"1/nAsWLsk/yOPl4sjynn6FOCC1ixnrbxSK9UHxjF8MQ="},"tlogEntries":[{"logIndex":"742710972","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1764918258","inclusionPromise":{"signedEntryTimestamp":"MEUCIFS+EyDQs1qXMw4vrcKjQzbCOt2I054mduiktKhfB+9TAiEAlqcvXPN1I2APZolO1Pcngj9NWN5Q/YYX6OmVb6mEt1o="},"inclusionProof":{"logIndex":"620806710","rootHash":"Lu0IpP9pvliM7/HLlERI61XXMxYaNagKKaoots59Les=","treeSize":"620806720","hashes":["CgtTK9zygUdrIszf3WhQNJMZOLaE1L6Rwl0uxZMzhrc=","G4zoKz8Qp4MzGIWpTiyFpMa+qNCXTHwv3wyF6fF9SqA=","4Zf1bJg5y3Mtao8mzWAIJOlUtX1Z3I/GaBqJoJcwM/w=","/5CtaBHwG4JWtPqT4ioXkxFryoJ9kt9y8/oeCSHPJtA=","rW3Qr9LepynlXRSmZGg7gaPEK8OVflSoXGVqsT8BTd4=","8Tuc79jz7XE8ShTZyuoWFvZJlJvtqhv3DW7G585PuIo=","XBSDZTLgOFArAxsVRI3+caaCjk+kN4xEqFELfZwmj/I=","O5NC4uRaoWv9gSsS91vdHBtUOLAtzQ7EaVjLZZM7KBE=","xaQ4EYppYG2jgzWLx0pu181jKiiPIHDp8ObrimpV+3k=","0jEq6eagxqoSOor9OR//fY6uOsPzLaE1q1n9tZRzfSc=","ZmUkYkHBy1B723JrEgiKvepTdHYrP6y2a4oODYvi5VY=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n620806720\nLu0IpP9pvliM7/HLlERI61XXMxYaNagKKaoots59Les=\n\n— rekor.sigstore.dev wNI9ajBFAiEA6sCIKTOf8ugkUn4VFfs1A1FAJqSKkJiyHLPvS9GXCVACICMCdlqMtYWVgfR2ZNQhkBK7kdpGehPKf/Py1YiQ858u\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIxNzA4OTJmNmE0OGIwYWVmNmY0MjZlYTk3YTg2ZjZjZDQ0MjBiYzUyNjM0ZjEyYTkyZjcyZTIwZjBmYTEyZTI5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQ0YUh1UFErWVFRdFBHZTMySFpLNlVsUUZIZFIzNVk4ckdSNEdWa0RWNGlRSWhBTmh3TzcwTHRDWkhOVkcrS2l0U2xST0JzUXkvOWpKcEpaNUtJV2lqL0Y2ViIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWm05Skt6bFNSa05VWTJacVpVMXhjRU5STTBaQmVYWkxkMEpSVlFwWlFVbE5NbU5tUkZJNFZ6azRUM2h1V0ZZcloyWldOVVJvWm05cE9IRnZaa0Z1Unk5MlF6ZEVZa0pzV0RKMEwyZFVOMGRMVlZwQlEyaEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyDADAgEAMIICvwYJKoZIhvcNAQcCoIICsDCCAqwCAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgo5l6GG8xXkhyhTfsxwek8V5JNiwtx+vqRSAFQ8wGWqMCFCj0ezXLUGFVOlZkIm9mo17QcBOXGA8yMDI1MTIwNTA3MDQxN1owAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdowggHWAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUxMjA1MDcwNDE3WjAvBgkqhkiG9w0BCQQxIgQgIbVrfaNg/B6wuaUYeihjczylc3PIQnohVf97LcorANUwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGYwZAIwRE/iD9RazHfiAEBZsXOX06c5h6LhDfrGj8nCN8E7WBIh6zttugY1E2HyV6nqfB8lAjAYY2wS6mXWT2SyGkOE7RFn/Bvr+B0o2Yr665j7XzS8znv6fVR//jNT1TGOeoblTgI="}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"FwiS9qSLCu9vQm6peob2zUQgvFJjTxKpL3LiDw+hLik="},"signature":"MEYCIQD4aHuPQ+YQQtPGe32HZK6UlQFHdR35Y8rGR4GVkDV4iQIhANhwO70LtCZHNVG+KitSlROBsQy/9jJpJZ5KIWij/F6V"}} \ No newline at end of file diff --git a/evidence-locker/signals/2025-12-05/heuristics_catalog.sigstore.json b/evidence-locker/signals/2025-12-05/heuristics_catalog.sigstore.json new file mode 100644 index 000000000..a57cc38f1 --- /dev/null +++ b/evidence-locker/signals/2025-12-05/heuristics_catalog.sigstore.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"1/nAsWLsk/yOPl4sjynn6FOCC1ixnrbxSK9UHxjF8MQ="},"tlogEntries":[{"logIndex":"742711114","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1764918263","inclusionPromise":{"signedEntryTimestamp":"MEQCID/YxgG8OSSwKwtN27ybyMZDjYjKHXrrJ4lhSoYyKupiAiAZYDZbz7B3S49KCUUHgH9kM1JMcgP+4GLskAQY7OAUTg=="},"inclusionProof":{"logIndex":"620806852","rootHash":"Wh++nnn319r7yZN5tW6uTz7FOyQm3gM5dK9idmPJ5r8=","treeSize":"620806857","hashes":["v0YshQ7k/R6/kZI7w6WEz7U+MibJYhtecUHQMMdocx4=","QQbQbfe/Xd+gjzRVbUOSg66NXJZ1XJUuyz/23lK8s/Q=","edAcwEEO/VnCUZNVF6quogbkR2VJA9PLsgXAcxRrxeo=","Aa5ouG8Qid7w54eR0dpN7Uvgj6IQODKO/O/kz1Pihy8=","ttHbUtEIyXg00SJXoHu92DyliFFdSQvcCNlj7Z9y4Qw=","a3wC4fF9YZKTB1QR421UwRxc0jy6ez8luUXH52yldD4=","XBSDZTLgOFArAxsVRI3+caaCjk+kN4xEqFELfZwmj/I=","O5NC4uRaoWv9gSsS91vdHBtUOLAtzQ7EaVjLZZM7KBE=","xaQ4EYppYG2jgzWLx0pu181jKiiPIHDp8ObrimpV+3k=","0jEq6eagxqoSOor9OR//fY6uOsPzLaE1q1n9tZRzfSc=","ZmUkYkHBy1B723JrEgiKvepTdHYrP6y2a4oODYvi5VY=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n620806857\nWh++nnn319r7yZN5tW6uTz7FOyQm3gM5dK9idmPJ5r8=\n\n— rekor.sigstore.dev wNI9ajBGAiEAlj4Wjq5n/xx/krvZHfN26Kj7N8sWBwhlWRc3xyA6v8ACIQDuKwyyO43OC+CUljxeoH+w58vTEyism+3q9U1tmtXz6g==\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJlMzNmYTA5NjM0OTMyNTJhNWFjMzc5YTEyZjgyMGY2YjM1NmVhOTQzMTBhZmQxZGI5YWQ3Mzk0ZTgzMDcwMDBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUQ2VGhHWUVYdUhPVS9KbUxHZ3RqRmJJSzdzaWoyc2hFZGl0S3dJUFlsYklRSWhBSjdpUFJ2UXpFalJubTk0NWtRaEJyNlowdE1jYkxRbGpjcHFBWGgyU1hFSSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWm05Skt6bFNSa05VWTJacVpVMXhjRU5STTBaQmVYWkxkMEpSVlFwWlFVbE5NbU5tUkZJNFZ6azRUM2h1V0ZZcloyWldOVVJvWm05cE9IRnZaa0Z1Unk5MlF6ZEVZa0pzV0RKMEwyZFVOMGRMVlZwQlEyaEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyDADAgEAMIICvwYJKoZIhvcNAQcCoIICsDCCAqwCAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg+NwUKgSfUN3RQ41fBNd60kI19DTMJmKkVsPnSYQVc9wCFDfXZ9XQ5d6aiO5wrX+EgfQKVjCiGA8yMDI1MTIwNTA3MDQyM1owAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdowggHWAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjUxMjA1MDcwNDIzWjAvBgkqhkiG9w0BCQQxIgQgyJgassB25DoacuBjxpkcGTSwdVAohy0f2Id3sSM6E1MwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGYwZAIwT7qihvUjMlnx37YkGsYMF5hNIzi8l5ZB3EgsxkR3Xzk2qtRwweY3s27UNLHPI2VrAjBORjkAKHamp9tFq348JW2vJ681PIWs7RY3x0DIEBANIcL/BCUhihWd8Qsy3zCFsSw="}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"4z+gljSTJSpaw3mhL4IPazVuqUMQr9Hbmtc5ToMHAA4="},"signature":"MEYCIQD6ThGYEXuHOU/JmLGgtjFbIK7sij2shEditKwIPYlbIQIhAJ7iPRvQzEjRnm945kQhBr6Z0tMcbLQljcpqAXh2SXEI"}} \ No newline at end of file diff --git a/evidence-locker/signals/2025-12-05/unknowns_scoring_manifest.sigstore.json b/evidence-locker/signals/2025-12-05/unknowns_scoring_manifest.sigstore.json new file mode 100644 index 000000000..d538d513e --- /dev/null +++ b/evidence-locker/signals/2025-12-05/unknowns_scoring_manifest.sigstore.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"publicKey":{"hint":"1/nAsWLsk/yOPl4sjynn6FOCC1ixnrbxSK9UHxjF8MQ="},"tlogEntries":[{"logIndex":"742711045","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1764918260","inclusionPromise":{"signedEntryTimestamp":"MEQCIBoK0UAl/VoKZhKHlxOHaG+hm/DV4rxCubqbXIl+nvzYAiAzzGial36KVwNItPI4jtl2dQYOo4/OLrH2Og64FtSDDQ=="},"inclusionProof":{"logIndex":"620806783","rootHash":"rnJM0HGHfkx44nH5GUAP4yBpG4qctPch1XzJYfyaVnw=","treeSize":"620806788","hashes":["qqxcFtTsJwXaS3xkPt7zsZevwEysqp/3itBaACycS1s=","Az48KauK71z/GtG9zKw/+AqVlMtdRVa2hx01sfismjY=","7GmbOI5m8AIevd8cFAP1DGBOPbFzDnFb267St3yKEBU=","G5W8OOGu8esamhiJbEOk8ZpbHThsBQFGlBw7Vr9ZI4M=","zEns3hpoSygqZ/DbdH5nxTXT6tm/iBJ7JDheeYAi3xI=","pKrfUkWRzOOrzrglojDfQe3Gw0MHrwnUNycqcb58NVo=","x3iICb7z+rvYiefmM8IHS+zVu+czvssQATmV0aauXR0=","hfptNxvGaE8SbKj83fMceYdlJp3zPi0z7rf9lhN7M3E=","XBSDZTLgOFArAxsVRI3+caaCjk+kN4xEqFELfZwmj/I=","O5NC4uRaoWv9gSsS91vdHBtUOLAtzQ7EaVjLZZM7KBE=","xaQ4EYppYG2jgzWLx0pu181jKiiPIHDp8ObrimpV+3k=","0jEq6eagxqoSOor9OR//fY6uOsPzLaE1q1n9tZRzfSc=","ZmUkYkHBy1B723JrEgiKvepTdHYrP6y2a4oODYvi5VY=","T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n620806788\nrnJM0HGHfkx44nH5GUAP4yBpG4qctPch1XzJYfyaVnw=\n\n— rekor.sigstore.dev wNI9ajBGAiEAmv0619ziIv5vAp64hQsjj9Cwtj2ukkmfYPgFsokjzDUCIQCr2VQuEKoEy9T55Kv1vxNIq5Cc8DkMZaCO7ELmDXYw0A==\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0NTA2NzUwMzU5MjhlNDc3MWNjYTFiOWU1ZjllNDIwMzVkYmUxMGIzZGU3YjY2YTQwNzdhN2I3MjliMmM1YjEzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNhS0w0TGNlNE5keDYrUnNWM1N4ZWNBbzRuR0E1L0Z5ZThncVU4eXRJUFFBSWhBTmkzbTkrRTZNM0xpSUZ4V0xvQzFMUG5yTHVDdUg3TWtOcmdpTUVOOUZhcyIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWm05Skt6bFNSa05VWTJacVpVMXhjRU5STTBaQmVYWkxkMEpSVlFwWlFVbE5NbU5tUkZJNFZ6azRUM2h1V0ZZcloyWldOVVJvWm05cE9IRnZaa0Z1Unk5MlF6ZEVZa0pzV0RKMEwyZFVOMGRMVlZwQlEyaEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgFvxA6M0TbvJyOio5F70mrIkWya1wHHwjPt27fTv+RP0CFQD9X2G+ujRRSCgSxfyL4T4lheLkzxgPMjAyNTEyMDUwNzA0MjBaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MTIwNTA3MDQyMFowLwYJKoZIhvcNAQkEMSIEIMu/+SX38aLih1/7aUTtRyRaBg8HMjjkOK/XDCLt6JHTMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMC0GoqwyqpVeD5ZUvRn88rG/N7W/cHcW8KD7sCs+aRmvN5fN6KbIWm6Of3T44OeXGQIxAL87AMyNjdSiNq6kfCS3X6cFQD5hQn+MdzBkEcP/jaWQlvk/M7UtwczuixFgUCVoMA=="}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"RQZ1A1ko5HccyhueX55CA12+ELPee2akB3p7cpssWxM="},"signature":"MEYCIQCaKL4Lce4Ndx6+RsV3SxecAo4nGA5/Fye8gqU8ytIPQAIhANi3m9+E6M3LiIFxWLoC1LPnrLuCuH7MkNrgiMEN9Fas"}} \ No newline at end of file diff --git a/scripts/packs/verify_offline_bundle.py b/scripts/packs/verify_offline_bundle.py new file mode 100644 index 000000000..6a350d366 --- /dev/null +++ b/scripts/packs/verify_offline_bundle.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +""" +Offline validator for Task Pack bundles. + +Validates the offline bundle manifest against a minimal rule set: + - Required fields (schemaVersion, pack, plan, evidence, security, hashes, slo, tenant/environment). + - Plan hash algorithm and canonical plan digest. + - Presence and SHA-256 digests for referenced files (inputs.lock, approvals ledger, SBOM, revocations, DSSE envelopes). + - Sandbox quotas/egress allowlist and SLO sanity. + +This script is deterministic: it emits sorted findings and never touches network resources. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +import sys +import tarfile +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional + + +@dataclass +class ValidationError: + path: str + message: str + + def __str__(self) -> str: + return f"{self.path}: {self.message}" + + +class BundleReader: + def __init__(self, bundle_path: str): + self.bundle_path = bundle_path + self._tar: Optional[tarfile.TarFile] = None + if tarfile.is_tarfile(bundle_path): + self._tar = tarfile.open(bundle_path, mode="r:*") + + def exists(self, path: str) -> bool: + if self._tar: + try: + self._tar.getmember(path) + return True + except KeyError: + return False + return os.path.exists(os.path.join(self.bundle_path, path)) + + def read_bytes(self, path: str) -> bytes: + if self._tar: + try: + member = self._tar.getmember(path) + except KeyError as exc: + raise FileNotFoundError(path) from exc + fileobj = self._tar.extractfile(member) + if fileobj is None: + raise FileNotFoundError(path) + return fileobj.read() + with open(os.path.join(self.bundle_path, path), "rb") as handle: + return handle.read() + + def close(self) -> None: + if self._tar: + self._tar.close() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Verify StellaOps Task Pack offline bundle deterministically." + ) + parser.add_argument( + "--bundle", + required=True, + help="Path to bundle directory or tarball containing bundle manifest + artefacts.", + ) + parser.add_argument( + "--manifest", + default="bundle.json", + help="Relative path to the offline bundle manifest inside the bundle (default: bundle.json).", + ) + parser.add_argument( + "--require-dsse", + action="store_true", + help="Fail if DSSE envelope files are missing.", + ) + return parser.parse_args() + + +def load_manifest(reader: BundleReader, manifest_path: str) -> Dict: + raw = reader.read_bytes(manifest_path) + try: + return json.loads(raw) + except json.JSONDecodeError as exc: + raise ValueError(f"Manifest {manifest_path} is not valid JSON: {exc}") from exc + + +def sha256_digest(data: bytes) -> str: + return f"sha256:{hashlib.sha256(data).hexdigest()}" + + +def parse_timestamp(value: str) -> bool: + try: + if value.endswith("Z"): + dt.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ") + else: + dt.datetime.fromisoformat(value) + return True + except (ValueError, TypeError): + return False + + +def validate_manifest(manifest: Dict) -> List[ValidationError]: + required_top = [ + "schemaVersion", + "pack", + "plan", + "evidence", + "security", + "hashes", + "slo", + "tenant", + "environment", + "created", + ] + errors: List[ValidationError] = [] + for key in required_top: + if key not in manifest: + errors.append(ValidationError(key, "is required")) + if manifest.get("schemaVersion") != "stellaops.pack.offline-bundle.v1": + errors.append( + ValidationError( + "schemaVersion", "must equal stellaops.pack.offline-bundle.v1" + ) + ) + plan = manifest.get("plan", {}) + if plan.get("hashAlgorithm") != "sha256": + errors.append(ValidationError("plan.hashAlgorithm", "must be sha256")) + for numeric_field in [ + ("slo.runP95Seconds", manifest.get("slo", {}).get("runP95Seconds")), + ("slo.approvalP95Seconds", manifest.get("slo", {}).get("approvalP95Seconds")), + ("slo.maxQueueDepth", manifest.get("slo", {}).get("maxQueueDepth")), + ]: + field, value = numeric_field + if value is None or not isinstance(value, (int, float)) or value <= 0: + errors.append(ValidationError(field, "must be > 0")) + for ts_field in ["created", "expires"]: + ts_value = manifest.get(ts_field) + if ts_value and not parse_timestamp(ts_value): + errors.append(ValidationError(ts_field, "must be ISO-8601 (UTC recommended)")) + sandbox = manifest.get("security", {}).get("sandbox", {}) + for quota_field in [ + ("security.sandbox.cpuLimitMillicores", sandbox.get("cpuLimitMillicores")), + ("security.sandbox.memoryLimitMiB", sandbox.get("memoryLimitMiB")), + ]: + field, value = quota_field + if value is None or not isinstance(value, (int, float)) or value <= 0: + errors.append(ValidationError(field, "must be > 0")) + allowlist = sandbox.get("egressAllowlist") + if allowlist is None or not isinstance(allowlist, list): + errors.append( + ValidationError( + "security.sandbox.egressAllowlist", + "must be an explicit list (empty list means fully sealed)", + ) + ) + return errors + + +def verify_hashes(reader: BundleReader, manifest: Dict) -> List[ValidationError]: + errors: List[ValidationError] = [] + hashes = manifest.get("hashes", []) + if not isinstance(hashes, list): + return [ValidationError("hashes", "must be an array of digest entries")] + for entry in hashes: + path = entry.get("path") + algo = entry.get("algorithm") + expected = entry.get("digest") + if not path or algo != "sha256" or not expected: + errors.append( + ValidationError( + f"hashes[{path or '?'}]", "path, algorithm=sha256, and digest are required" + ) + ) + continue + if not reader.exists(path): + errors.append(ValidationError(path, "referenced in hashes but missing from bundle")) + continue + actual = sha256_digest(reader.read_bytes(path)) + if actual != expected: + errors.append( + ValidationError( + path, f"digest mismatch (expected {expected}, got {actual})" + ) + ) + return errors + + +def verify_files(reader: BundleReader, manifest: Dict, require_dsse: bool) -> List[ValidationError]: + errors: List[ValidationError] = [] + paths = [ + ("plan.canonicalPlanPath", manifest.get("plan", {}).get("canonicalPlanPath")), + ("plan.inputsLock", manifest.get("plan", {}).get("inputsLock")), + ("pack.sbom", manifest.get("pack", {}).get("sbom")), + ("evidence.attestation", manifest.get("evidence", {}).get("attestation")), + ("evidence.approvalsLedger", manifest.get("evidence", {}).get("approvalsLedger")), + ("security.revocations", manifest.get("security", {}).get("revocations")), + ("security.secretsRedactionPolicy", manifest.get("security", {}).get("secretsRedactionPolicy")), + ] + if require_dsse: + signatures = manifest.get("security", {}).get("signatures", {}) + paths.extend( + [ + ("security.signatures.bundleDsse", signatures.get("bundleDsse")), + ("security.signatures.attestationDsse", signatures.get("attestationDsse")), + ] + ) + missing = [ + ValidationError(path_label, "file path missing") + for path_label, path_value in paths + if not path_value + ] + for err in missing: + errors.append(err) + for path_label, path_value in paths: + if not path_value: + continue + if not reader.exists(path_value): + errors.append(ValidationError(path_label, f"{path_value} not found in bundle")) + # Verify canonical plan hash if the file exists. + plan_path = manifest.get("plan", {}).get("canonicalPlanPath") + expected_plan_hash = manifest.get("plan", {}).get("hash") + if plan_path and expected_plan_hash and reader.exists(plan_path): + actual_plan_hash = sha256_digest(reader.read_bytes(plan_path)) + if actual_plan_hash != expected_plan_hash: + errors.append( + ValidationError( + "plan.hash", + f"hash mismatch (expected {expected_plan_hash}, got {actual_plan_hash})", + ) + ) + return errors + + +def main() -> int: + args = parse_args() + reader = BundleReader(args.bundle) + try: + manifest = load_manifest(reader, args.manifest) + errors: List[ValidationError] = [] + errors.extend(validate_manifest(manifest)) + errors.extend(verify_files(reader, manifest, require_dsse=args.require_dsse)) + errors.extend(verify_hashes(reader, manifest)) + if errors: + for item in sorted(errors, key=lambda e: e.path): + print(f"[FAIL] {item}") + return 1 + print("[OK] offline bundle validated") + return 0 + finally: + reader.close() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerAuditSink.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerAuditSink.cs new file mode 100644 index 000000000..d11dcbf84 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerAuditSink.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Npgsql; +using NpgsqlTypes; +using StellaOps.IssuerDirectory.Core.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Repositories; + +/// +/// PostgreSQL implementation of the issuer audit sink. +/// +public sealed class PostgresIssuerAuditSink : IIssuerAuditSink +{ + private readonly IssuerDirectoryDataSource _dataSource; + private readonly ILogger _logger; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + public PostgresIssuerAuditSink(IssuerDirectoryDataSource dataSource, ILogger logger) + { + _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task WriteAsync(IssuerAuditEntry entry, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(entry); + + await using var connection = await _dataSource.OpenConnectionAsync(entry.TenantId, "writer", cancellationToken).ConfigureAwait(false); + + const string sql = """ + INSERT INTO issuer.audit (tenant_id, actor, action, issuer_id, reason, details, occurred_at) + VALUES (@tenantId::uuid, @actor, @action, @issuerId::uuid, @reason, @details::jsonb, @occurredAt) + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + + command.Parameters.AddWithValue("tenantId", Guid.Parse(entry.TenantId)); + command.Parameters.AddWithValue("actor", entry.Actor); + command.Parameters.AddWithValue("action", entry.Action); + command.Parameters.AddWithValue("issuerId", Guid.Parse(entry.IssuerId)); + command.Parameters.Add(new NpgsqlParameter("reason", NpgsqlDbType.Text) { Value = entry.Reason ?? (object)DBNull.Value }); + command.Parameters.Add(new NpgsqlParameter("details", NpgsqlDbType.Jsonb) { Value = SerializeMetadata(entry.Metadata) }); + command.Parameters.AddWithValue("occurredAt", entry.TimestampUtc.UtcDateTime); + + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + _logger.LogDebug("Wrote audit entry: {Action} for issuer {IssuerId} by {Actor}.", entry.Action, entry.IssuerId, entry.Actor); + } + + private static string SerializeMetadata(IReadOnlyDictionary metadata) + { + if (metadata.Count == 0) + { + return "{}"; + } + + return JsonSerializer.Serialize(metadata, JsonOptions); + } +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerKeyRepository.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerKeyRepository.cs new file mode 100644 index 000000000..188a85a39 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerKeyRepository.cs @@ -0,0 +1,241 @@ +using Microsoft.Extensions.Logging; +using Npgsql; +using NpgsqlTypes; +using StellaOps.IssuerDirectory.Core.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Repositories; + +/// +/// PostgreSQL implementation of the issuer key repository. +/// +public sealed class PostgresIssuerKeyRepository : IIssuerKeyRepository +{ + private readonly IssuerDirectoryDataSource _dataSource; + private readonly ILogger _logger; + + public PostgresIssuerKeyRepository(IssuerDirectoryDataSource dataSource, ILogger logger) + { + _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task GetAsync(string tenantId, string issuerId, string keyId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, issuer_id, tenant_id, key_id, key_type, public_key, fingerprint, not_before, not_after, status, replaces_key_id, created_at, created_by, updated_at, updated_by, retired_at, revoked_at, revoke_reason, metadata + FROM issuer.issuer_keys + WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid AND key_id = @keyId + LIMIT 1 + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + command.Parameters.AddWithValue("keyId", keyId); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + return null; + } + + return MapToRecord(reader); + } + + public async Task GetByFingerprintAsync(string tenantId, string issuerId, string fingerprint, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, issuer_id, tenant_id, key_id, key_type, public_key, fingerprint, not_before, not_after, status, replaces_key_id, created_at, created_by, updated_at, updated_by, retired_at, revoked_at, revoke_reason, metadata + FROM issuer.issuer_keys + WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid AND fingerprint = @fingerprint + LIMIT 1 + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + command.Parameters.AddWithValue("fingerprint", fingerprint); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + return null; + } + + return MapToRecord(reader); + } + + public async Task> ListAsync(string tenantId, string issuerId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, issuer_id, tenant_id, key_id, key_type, public_key, fingerprint, not_before, not_after, status, replaces_key_id, created_at, created_by, updated_at, updated_by, retired_at, revoked_at, revoke_reason, metadata + FROM issuer.issuer_keys + WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid + ORDER BY created_at ASC + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + + return await ReadAllRecordsAsync(command, cancellationToken).ConfigureAwait(false); + } + + public async Task> ListGlobalAsync(string issuerId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, issuer_id, tenant_id, key_id, key_type, public_key, fingerprint, not_before, not_after, status, replaces_key_id, created_at, created_by, updated_at, updated_by, retired_at, revoked_at, revoke_reason, metadata + FROM issuer.issuer_keys + WHERE tenant_id = @globalTenantId::uuid AND issuer_id = @issuerId::uuid + ORDER BY created_at ASC + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("globalTenantId", IssuerTenants.Global); + command.Parameters.AddWithValue("issuerId", issuerId); + + return await ReadAllRecordsAsync(command, cancellationToken).ConfigureAwait(false); + } + + public async Task UpsertAsync(IssuerKeyRecord record, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(record); + + await using var connection = await _dataSource.OpenConnectionAsync(record.TenantId, "writer", cancellationToken).ConfigureAwait(false); + + const string sql = """ + INSERT INTO issuer.issuer_keys (id, issuer_id, tenant_id, key_id, key_type, public_key, fingerprint, not_before, not_after, status, replaces_key_id, created_at, created_by, updated_at, updated_by, retired_at, revoked_at, revoke_reason, metadata) + VALUES (@id::uuid, @issuerId::uuid, @tenantId::uuid, @keyId, @keyType, @publicKey, @fingerprint, @notBefore, @notAfter, @status, @replacesKeyId, @createdAt, @createdBy, @updatedAt, @updatedBy, @retiredAt, @revokedAt, @revokeReason, @metadata::jsonb) + ON CONFLICT (issuer_id, key_id) + DO UPDATE SET + key_type = EXCLUDED.key_type, + public_key = EXCLUDED.public_key, + fingerprint = EXCLUDED.fingerprint, + not_before = EXCLUDED.not_before, + not_after = EXCLUDED.not_after, + status = EXCLUDED.status, + replaces_key_id = EXCLUDED.replaces_key_id, + updated_at = EXCLUDED.updated_at, + updated_by = EXCLUDED.updated_by, + retired_at = EXCLUDED.retired_at, + revoked_at = EXCLUDED.revoked_at, + revoke_reason = EXCLUDED.revoke_reason, + metadata = EXCLUDED.metadata + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + + command.Parameters.AddWithValue("id", Guid.Parse(record.Id)); + command.Parameters.AddWithValue("issuerId", Guid.Parse(record.IssuerId)); + command.Parameters.AddWithValue("tenantId", Guid.Parse(record.TenantId)); + command.Parameters.AddWithValue("keyId", record.Id); + command.Parameters.AddWithValue("keyType", MapKeyType(record.Type)); + command.Parameters.AddWithValue("publicKey", record.Material.Value); + command.Parameters.AddWithValue("fingerprint", record.Fingerprint); + command.Parameters.Add(new NpgsqlParameter("notBefore", NpgsqlDbType.TimestampTz) { Value = (object?)null ?? DBNull.Value }); + command.Parameters.Add(new NpgsqlParameter("notAfter", NpgsqlDbType.TimestampTz) { Value = record.ExpiresAtUtc.HasValue ? record.ExpiresAtUtc.Value.UtcDateTime : DBNull.Value }); + command.Parameters.AddWithValue("status", record.Status.ToString().ToLowerInvariant()); + command.Parameters.Add(new NpgsqlParameter("replacesKeyId", NpgsqlDbType.Text) { Value = record.ReplacesKeyId ?? (object)DBNull.Value }); + command.Parameters.AddWithValue("createdAt", record.CreatedAtUtc.UtcDateTime); + command.Parameters.AddWithValue("createdBy", record.CreatedBy); + command.Parameters.AddWithValue("updatedAt", record.UpdatedAtUtc.UtcDateTime); + command.Parameters.AddWithValue("updatedBy", record.UpdatedBy); + command.Parameters.Add(new NpgsqlParameter("retiredAt", NpgsqlDbType.TimestampTz) { Value = record.RetiredAtUtc.HasValue ? record.RetiredAtUtc.Value.UtcDateTime : DBNull.Value }); + command.Parameters.Add(new NpgsqlParameter("revokedAt", NpgsqlDbType.TimestampTz) { Value = record.RevokedAtUtc.HasValue ? record.RevokedAtUtc.Value.UtcDateTime : DBNull.Value }); + command.Parameters.Add(new NpgsqlParameter("revokeReason", NpgsqlDbType.Text) { Value = DBNull.Value }); + command.Parameters.Add(new NpgsqlParameter("metadata", NpgsqlDbType.Jsonb) { Value = "{}" }); + + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + _logger.LogDebug("Upserted issuer key {KeyId} for issuer {IssuerId}.", record.Id, record.IssuerId); + } + + private static async Task> ReadAllRecordsAsync(NpgsqlCommand command, CancellationToken cancellationToken) + { + var results = new List(); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + results.Add(MapToRecord(reader)); + } + return results; + } + + private static IssuerKeyRecord MapToRecord(NpgsqlDataReader reader) + { + var id = reader.GetGuid(0).ToString(); + var issuerId = reader.GetGuid(1).ToString(); + var tenantId = reader.GetGuid(2).ToString(); + var keyId = reader.GetString(3); + var keyType = ParseKeyType(reader.GetString(4)); + var publicKey = reader.GetString(5); + var fingerprint = reader.GetString(6); + var notBefore = reader.IsDBNull(7) ? (DateTimeOffset?)null : new DateTimeOffset(reader.GetDateTime(7), TimeSpan.Zero); + var notAfter = reader.IsDBNull(8) ? (DateTimeOffset?)null : new DateTimeOffset(reader.GetDateTime(8), TimeSpan.Zero); + var status = ParseKeyStatus(reader.GetString(9)); + var replacesKeyId = reader.IsDBNull(10) ? null : reader.GetString(10); + var createdAt = reader.GetDateTime(11); + var createdBy = reader.GetString(12); + var updatedAt = reader.GetDateTime(13); + var updatedBy = reader.GetString(14); + var retiredAt = reader.IsDBNull(15) ? (DateTimeOffset?)null : new DateTimeOffset(reader.GetDateTime(15), TimeSpan.Zero); + var revokedAt = reader.IsDBNull(16) ? (DateTimeOffset?)null : new DateTimeOffset(reader.GetDateTime(16), TimeSpan.Zero); + + return new IssuerKeyRecord + { + Id = keyId, + IssuerId = issuerId, + TenantId = tenantId, + Type = keyType, + Status = status, + Material = new IssuerKeyMaterial("pem", publicKey), + Fingerprint = fingerprint, + CreatedAtUtc = new DateTimeOffset(createdAt, TimeSpan.Zero), + CreatedBy = createdBy, + UpdatedAtUtc = new DateTimeOffset(updatedAt, TimeSpan.Zero), + UpdatedBy = updatedBy, + ExpiresAtUtc = notAfter, + RetiredAtUtc = retiredAt, + RevokedAtUtc = revokedAt, + ReplacesKeyId = replacesKeyId + }; + } + + private static string MapKeyType(IssuerKeyType type) => type switch + { + IssuerKeyType.Ed25519PublicKey => "ed25519", + IssuerKeyType.X509Certificate => "x509", + IssuerKeyType.DssePublicKey => "dsse", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unsupported key type") + }; + + private static IssuerKeyType ParseKeyType(string value) => value.ToLowerInvariant() switch + { + "ed25519" => IssuerKeyType.Ed25519PublicKey, + "x509" => IssuerKeyType.X509Certificate, + "dsse" => IssuerKeyType.DssePublicKey, + _ => throw new ArgumentException($"Unknown key type: {value}", nameof(value)) + }; + + private static IssuerKeyStatus ParseKeyStatus(string value) => value.ToLowerInvariant() switch + { + "active" => IssuerKeyStatus.Active, + "retired" => IssuerKeyStatus.Retired, + "revoked" => IssuerKeyStatus.Revoked, + _ => throw new ArgumentException($"Unknown key status: {value}", nameof(value)) + }; +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerRepository.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerRepository.cs new file mode 100644 index 000000000..c0a20f099 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerRepository.cs @@ -0,0 +1,317 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Npgsql; +using NpgsqlTypes; +using StellaOps.IssuerDirectory.Core.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Repositories; + +/// +/// PostgreSQL implementation of the issuer repository. +/// +public sealed class PostgresIssuerRepository : IIssuerRepository +{ + private readonly IssuerDirectoryDataSource _dataSource; + private readonly ILogger _logger; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + public PostgresIssuerRepository(IssuerDirectoryDataSource dataSource, ILogger logger) + { + _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task GetAsync(string tenantId, string issuerId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, tenant_id, name, display_name, description, endpoints, contact, metadata, tags, status, is_system_seed, created_at, created_by, updated_at, updated_by + FROM issuer.issuers + WHERE tenant_id = @tenantId::uuid AND id = @issuerId::uuid + LIMIT 1 + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + return null; + } + + return MapToRecord(reader); + } + + public async Task> ListAsync(string tenantId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, tenant_id, name, display_name, description, endpoints, contact, metadata, tags, status, is_system_seed, created_at, created_by, updated_at, updated_by + FROM issuer.issuers + WHERE tenant_id = @tenantId::uuid + ORDER BY name ASC + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + + return await ReadAllRecordsAsync(command, cancellationToken).ConfigureAwait(false); + } + + public async Task> ListGlobalAsync(CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, tenant_id, name, display_name, description, endpoints, contact, metadata, tags, status, is_system_seed, created_at, created_by, updated_at, updated_by + FROM issuer.issuers + WHERE tenant_id = @globalTenantId::uuid + ORDER BY name ASC + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("globalTenantId", IssuerTenants.Global); + + return await ReadAllRecordsAsync(command, cancellationToken).ConfigureAwait(false); + } + + public async Task UpsertAsync(IssuerRecord record, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(record); + + await using var connection = await _dataSource.OpenConnectionAsync(record.TenantId, "writer", cancellationToken).ConfigureAwait(false); + + const string sql = """ + INSERT INTO issuer.issuers (id, tenant_id, name, display_name, description, endpoints, contact, metadata, tags, status, is_system_seed, created_at, created_by, updated_at, updated_by) + VALUES (@id::uuid, @tenantId::uuid, @name, @displayName, @description, @endpoints::jsonb, @contact::jsonb, @metadata::jsonb, @tags, @status, @isSystemSeed, @createdAt, @createdBy, @updatedAt, @updatedBy) + ON CONFLICT (tenant_id, name) + DO UPDATE SET + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + endpoints = EXCLUDED.endpoints, + contact = EXCLUDED.contact, + metadata = EXCLUDED.metadata, + tags = EXCLUDED.tags, + status = EXCLUDED.status, + updated_at = EXCLUDED.updated_at, + updated_by = EXCLUDED.updated_by + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + + command.Parameters.AddWithValue("id", Guid.Parse(record.Id)); + command.Parameters.AddWithValue("tenantId", Guid.Parse(record.TenantId)); + command.Parameters.AddWithValue("name", record.Slug); + command.Parameters.AddWithValue("displayName", record.DisplayName); + command.Parameters.Add(new NpgsqlParameter("description", NpgsqlDbType.Text) { Value = record.Description ?? (object)DBNull.Value }); + command.Parameters.Add(new NpgsqlParameter("endpoints", NpgsqlDbType.Jsonb) { Value = SerializeEndpoints(record.Endpoints) }); + command.Parameters.Add(new NpgsqlParameter("contact", NpgsqlDbType.Jsonb) { Value = SerializeContact(record.Contact) }); + command.Parameters.Add(new NpgsqlParameter("metadata", NpgsqlDbType.Jsonb) { Value = SerializeMetadata(record.Metadata) }); + command.Parameters.Add(new NpgsqlParameter("tags", NpgsqlDbType.Array | NpgsqlDbType.Text) { Value = record.Tags.ToArray() }); + command.Parameters.AddWithValue("status", "active"); + command.Parameters.AddWithValue("isSystemSeed", record.IsSystemSeed); + command.Parameters.AddWithValue("createdAt", record.CreatedAtUtc); + command.Parameters.AddWithValue("createdBy", record.CreatedBy); + command.Parameters.AddWithValue("updatedAt", record.UpdatedAtUtc); + command.Parameters.AddWithValue("updatedBy", record.UpdatedBy); + + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + _logger.LogDebug("Upserted issuer {IssuerId} for tenant {TenantId}.", record.Id, record.TenantId); + } + + public async Task DeleteAsync(string tenantId, string issuerId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false); + + const string sql = "DELETE FROM issuer.issuers WHERE tenant_id = @tenantId::uuid AND id = @issuerId::uuid"; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + + var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + _logger.LogDebug("Deleted issuer {IssuerId} for tenant {TenantId}. Rows affected: {Rows}.", issuerId, tenantId, rowsAffected); + } + + private static async Task> ReadAllRecordsAsync(NpgsqlCommand command, CancellationToken cancellationToken) + { + var results = new List(); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + results.Add(MapToRecord(reader)); + } + return results; + } + + private static IssuerRecord MapToRecord(NpgsqlDataReader reader) + { + var id = reader.GetGuid(0).ToString(); + var tenantId = reader.GetGuid(1).ToString(); + var name = reader.GetString(2); + var displayName = reader.GetString(3); + var description = reader.IsDBNull(4) ? null : reader.GetString(4); + var endpointsJson = reader.GetString(5); + var contactJson = reader.GetString(6); + var metadataJson = reader.GetString(7); + var tags = reader.GetFieldValue(8); + var isSystemSeed = reader.GetBoolean(10); + var createdAt = reader.GetDateTime(11); + var createdBy = reader.GetString(12); + var updatedAt = reader.GetDateTime(13); + var updatedBy = reader.GetString(14); + + var contact = DeserializeContact(contactJson); + var metadata = DeserializeMetadata(metadataJson); + var endpoints = DeserializeEndpoints(endpointsJson); + + return new IssuerRecord + { + Id = id, + TenantId = tenantId, + Slug = name, + DisplayName = displayName, + Description = description, + Contact = contact, + Metadata = metadata, + Endpoints = endpoints, + Tags = tags, + IsSystemSeed = isSystemSeed, + CreatedAtUtc = new DateTimeOffset(createdAt, TimeSpan.Zero), + CreatedBy = createdBy, + UpdatedAtUtc = new DateTimeOffset(updatedAt, TimeSpan.Zero), + UpdatedBy = updatedBy + }; + } + + private static string SerializeContact(IssuerContact contact) + { + var doc = new + { + email = contact.Email, + phone = contact.Phone, + website = contact.Website?.ToString(), + timezone = contact.Timezone + }; + return JsonSerializer.Serialize(doc, JsonOptions); + } + + private static IssuerContact DeserializeContact(string json) + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var email = root.TryGetProperty("email", out var e) && e.ValueKind != JsonValueKind.Null ? e.GetString() : null; + var phone = root.TryGetProperty("phone", out var p) && p.ValueKind != JsonValueKind.Null ? p.GetString() : null; + var websiteStr = root.TryGetProperty("website", out var w) && w.ValueKind != JsonValueKind.Null ? w.GetString() : null; + var timezone = root.TryGetProperty("timezone", out var t) && t.ValueKind != JsonValueKind.Null ? t.GetString() : null; + return new IssuerContact(email, phone, string.IsNullOrWhiteSpace(websiteStr) ? null : new Uri(websiteStr), timezone); + } + + private static string SerializeMetadata(IssuerMetadata metadata) + { + var doc = new + { + cveOrgId = metadata.CveOrgId, + csafPublisherId = metadata.CsafPublisherId, + securityAdvisoriesUrl = metadata.SecurityAdvisoriesUrl?.ToString(), + catalogUrl = metadata.CatalogUrl?.ToString(), + languages = metadata.SupportedLanguages.ToList(), + attributes = new Dictionary(metadata.Attributes) + }; + return JsonSerializer.Serialize(doc, JsonOptions); + } + + private static IssuerMetadata DeserializeMetadata(string json) + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var cveOrgId = root.TryGetProperty("cveOrgId", out var c) && c.ValueKind != JsonValueKind.Null ? c.GetString() : null; + var csafPublisherId = root.TryGetProperty("csafPublisherId", out var cp) && cp.ValueKind != JsonValueKind.Null ? cp.GetString() : null; + var securityAdvisoriesUrlStr = root.TryGetProperty("securityAdvisoriesUrl", out var sa) && sa.ValueKind != JsonValueKind.Null ? sa.GetString() : null; + var catalogUrlStr = root.TryGetProperty("catalogUrl", out var cu) && cu.ValueKind != JsonValueKind.Null ? cu.GetString() : null; + + var languages = new List(); + if (root.TryGetProperty("languages", out var langs) && langs.ValueKind == JsonValueKind.Array) + { + foreach (var lang in langs.EnumerateArray()) + { + if (lang.ValueKind == JsonValueKind.String) + { + languages.Add(lang.GetString()!); + } + } + } + + var attributes = new Dictionary(); + if (root.TryGetProperty("attributes", out var attrs) && attrs.ValueKind == JsonValueKind.Object) + { + foreach (var prop in attrs.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.String) + { + attributes[prop.Name] = prop.Value.GetString()!; + } + } + } + + return new IssuerMetadata( + cveOrgId, + csafPublisherId, + string.IsNullOrWhiteSpace(securityAdvisoriesUrlStr) ? null : new Uri(securityAdvisoriesUrlStr), + string.IsNullOrWhiteSpace(catalogUrlStr) ? null : new Uri(catalogUrlStr), + languages, + attributes); + } + + private static string SerializeEndpoints(IReadOnlyCollection endpoints) + { + var docs = endpoints.Select(e => new + { + kind = e.Kind, + url = e.Url.ToString(), + format = e.Format, + requiresAuthentication = e.RequiresAuthentication + }).ToList(); + return JsonSerializer.Serialize(docs, JsonOptions); + } + + private static IReadOnlyCollection DeserializeEndpoints(string json) + { + var results = new List(); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.ValueKind != JsonValueKind.Array) + { + return results; + } + + foreach (var elem in doc.RootElement.EnumerateArray()) + { + var kind = elem.TryGetProperty("kind", out var k) ? k.GetString() : null; + var urlStr = elem.TryGetProperty("url", out var u) ? u.GetString() : null; + var format = elem.TryGetProperty("format", out var f) && f.ValueKind != JsonValueKind.Null ? f.GetString() : null; + var requiresAuth = elem.TryGetProperty("requiresAuthentication", out var ra) && ra.GetBoolean(); + + if (!string.IsNullOrWhiteSpace(kind) && !string.IsNullOrWhiteSpace(urlStr)) + { + results.Add(new IssuerEndpoint(kind, new Uri(urlStr), format, requiresAuth)); + } + } + + return results; + } +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerTrustRepository.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerTrustRepository.cs new file mode 100644 index 000000000..e764345b1 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/Repositories/PostgresIssuerTrustRepository.cs @@ -0,0 +1,120 @@ +using Microsoft.Extensions.Logging; +using Npgsql; +using NpgsqlTypes; +using StellaOps.IssuerDirectory.Core.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Repositories; + +/// +/// PostgreSQL implementation of the issuer trust repository. +/// +public sealed class PostgresIssuerTrustRepository : IIssuerTrustRepository +{ + private readonly IssuerDirectoryDataSource _dataSource; + private readonly ILogger _logger; + + public PostgresIssuerTrustRepository(IssuerDirectoryDataSource dataSource, ILogger logger) + { + _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task GetAsync(string tenantId, string issuerId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + const string sql = """ + SELECT id, issuer_id, tenant_id, weight, rationale, expires_at, created_at, created_by, updated_at, updated_by + FROM issuer.trust_overrides + WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid + LIMIT 1 + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + return null; + } + + return MapToRecord(reader); + } + + public async Task UpsertAsync(IssuerTrustOverrideRecord record, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(record); + + await using var connection = await _dataSource.OpenConnectionAsync(record.TenantId, "writer", cancellationToken).ConfigureAwait(false); + + const string sql = """ + INSERT INTO issuer.trust_overrides (issuer_id, tenant_id, weight, rationale, created_at, created_by, updated_at, updated_by) + VALUES (@issuerId::uuid, @tenantId::uuid, @weight, @rationale, @createdAt, @createdBy, @updatedAt, @updatedBy) + ON CONFLICT (issuer_id, tenant_id) + DO UPDATE SET + weight = EXCLUDED.weight, + rationale = EXCLUDED.rationale, + updated_at = EXCLUDED.updated_at, + updated_by = EXCLUDED.updated_by + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + + command.Parameters.AddWithValue("issuerId", Guid.Parse(record.IssuerId)); + command.Parameters.AddWithValue("tenantId", Guid.Parse(record.TenantId)); + command.Parameters.AddWithValue("weight", record.Weight); + command.Parameters.Add(new NpgsqlParameter("rationale", NpgsqlDbType.Text) { Value = record.Reason ?? (object)DBNull.Value }); + command.Parameters.AddWithValue("createdAt", record.CreatedAtUtc.UtcDateTime); + command.Parameters.AddWithValue("createdBy", record.CreatedBy); + command.Parameters.AddWithValue("updatedAt", record.UpdatedAtUtc.UtcDateTime); + command.Parameters.AddWithValue("updatedBy", record.UpdatedBy); + + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + + _logger.LogDebug("Upserted trust override for issuer {IssuerId} in tenant {TenantId}.", record.IssuerId, record.TenantId); + } + + public async Task DeleteAsync(string tenantId, string issuerId, CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "writer", cancellationToken).ConfigureAwait(false); + + const string sql = "DELETE FROM issuer.trust_overrides WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid"; + + await using var command = new NpgsqlCommand(sql, connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenantId", tenantId); + command.Parameters.AddWithValue("issuerId", issuerId); + + var rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + _logger.LogDebug("Deleted trust override for issuer {IssuerId} in tenant {TenantId}. Rows affected: {Rows}.", issuerId, tenantId, rowsAffected); + } + + private static IssuerTrustOverrideRecord MapToRecord(NpgsqlDataReader reader) + { + var issuerId = reader.GetGuid(1).ToString(); + var tenantId = reader.GetGuid(2).ToString(); + var weight = reader.GetDecimal(3); + var rationale = reader.IsDBNull(4) ? null : reader.GetString(4); + var createdAt = reader.GetDateTime(6); + var createdBy = reader.GetString(7); + var updatedAt = reader.GetDateTime(8); + var updatedBy = reader.GetString(9); + + return new IssuerTrustOverrideRecord + { + IssuerId = issuerId, + TenantId = tenantId, + Weight = weight, + Reason = rationale, + CreatedAtUtc = new DateTimeOffset(createdAt, TimeSpan.Zero), + CreatedBy = createdBy, + UpdatedAtUtc = new DateTimeOffset(updatedAt, TimeSpan.Zero), + UpdatedBy = updatedBy + }; + } +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/ServiceCollectionExtensions.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/ServiceCollectionExtensions.cs index 10c9a9ffd..4a7222e6b 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/ServiceCollectionExtensions.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.Storage.Postgres/ServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Infrastructure.Postgres.Options; +using StellaOps.IssuerDirectory.Core.Abstractions; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; namespace StellaOps.IssuerDirectory.Storage.Postgres; @@ -34,6 +36,8 @@ public static class ServiceCollectionExtensions return new IssuerDirectoryDataSource(options, logger); }); + RegisterRepositories(services); + return services; } @@ -61,6 +65,16 @@ public static class ServiceCollectionExtensions return new IssuerDirectoryDataSource(options, logger); }); + RegisterRepositories(services); + return services; } + + private static void RegisterRepositories(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } } diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Options/IssuerDirectoryWebServiceOptions.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Options/IssuerDirectoryWebServiceOptions.cs index e9842b40b..db4be50e4 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Options/IssuerDirectoryWebServiceOptions.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Options/IssuerDirectoryWebServiceOptions.cs @@ -10,6 +10,8 @@ public sealed class IssuerDirectoryWebServiceOptions public AuthorityOptions Authority { get; set; } = new(); + public PersistenceOptions Persistence { get; set; } = new(); + public string TenantHeader { get; set; } = "X-StellaOps-Tenant"; public bool SeedCsafPublishers { get; set; } = true; @@ -24,6 +26,7 @@ public sealed class IssuerDirectoryWebServiceOptions } Authority.Validate(); + Persistence.Validate(); } public sealed class TelemetryOptions @@ -74,4 +77,31 @@ public sealed class IssuerDirectoryWebServiceOptions } } } + + public sealed class PersistenceOptions + { + /// + /// Storage provider for IssuerDirectory. Valid values: "Mongo", "Postgres". + /// + public string Provider { get; set; } = "Mongo"; + + /// + /// PostgreSQL connection string. Required when Provider is "Postgres". + /// + public string PostgresConnectionString { get; set; } = string.Empty; + + public void Validate() + { + var normalized = Provider?.Trim().ToLowerInvariant() ?? string.Empty; + if (normalized != "mongo" && normalized != "postgres") + { + throw new InvalidOperationException($"IssuerDirectory persistence provider '{Provider}' is not supported. Use 'Mongo' or 'Postgres'."); + } + + if (normalized == "postgres" && string.IsNullOrWhiteSpace(PostgresConnectionString)) + { + throw new InvalidOperationException("PostgreSQL connection string is required when persistence provider is 'Postgres'."); + } + } + } } diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs index b659e41b0..b26cc07d4 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs @@ -14,7 +14,9 @@ using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; using StellaOps.Configuration; using StellaOps.IssuerDirectory.Core.Services; +using StellaOps.Infrastructure.Postgres.Options; using StellaOps.IssuerDirectory.Infrastructure; +using StellaOps.IssuerDirectory.Storage.Postgres; using StellaOps.IssuerDirectory.Infrastructure.Seed; using StellaOps.IssuerDirectory.WebService.Endpoints; using StellaOps.IssuerDirectory.WebService.Options; @@ -58,7 +60,10 @@ builder.Services.AddOptions() builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddProblemDetails(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddIssuerDirectoryInfrastructure(builder.Configuration); + +// Configure persistence based on provider +ConfigurePersistence(builder, bootstrapOptions); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -112,6 +117,28 @@ static LogEventLevel MapLogLevel(string? value) : LogEventLevel.Information; } +static void ConfigurePersistence( + WebApplicationBuilder builder, + IssuerDirectoryWebServiceOptions options) +{ + var provider = options.Persistence.Provider?.Trim().ToLowerInvariant() ?? "mongo"; + + if (provider == "postgres") + { + Log.Information("Using PostgreSQL persistence for IssuerDirectory."); + builder.Services.AddIssuerDirectoryPostgresStorage(new PostgresOptions + { + ConnectionString = options.Persistence.PostgresConnectionString, + SchemaName = "issuer" + }); + } + else + { + Log.Information("Using MongoDB persistence for IssuerDirectory."); + builder.Services.AddIssuerDirectoryInfrastructure(builder.Configuration); + } +} + static void ConfigureAuthentication( WebApplicationBuilder builder, IssuerDirectoryWebServiceOptions options) diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/StellaOps.IssuerDirectory.WebService.csproj b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/StellaOps.IssuerDirectory.WebService.csproj index a7eb1d194..98302732c 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/StellaOps.IssuerDirectory.WebService.csproj +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/StellaOps.IssuerDirectory.WebService.csproj @@ -18,6 +18,7 @@ + diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerAuditSinkTests.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerAuditSinkTests.cs new file mode 100644 index 000000000..b438d961d --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerAuditSinkTests.cs @@ -0,0 +1,248 @@ +using System.Text.Json; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Npgsql; +using StellaOps.Infrastructure.Postgres.Options; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +[Collection(IssuerDirectoryPostgresCollection.Name)] +public sealed class IssuerAuditSinkTests : IAsyncLifetime +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + private readonly PostgresIssuerRepository _issuerRepository; + private readonly PostgresIssuerAuditSink _auditSink; + private readonly IssuerDirectoryDataSource _dataSource; + private readonly string _tenantId = Guid.NewGuid().ToString(); + private string _issuerId = null!; + + public IssuerAuditSinkTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + + var options = new PostgresOptions + { + ConnectionString = fixture.ConnectionString, + SchemaName = fixture.SchemaName + }; + _dataSource = new IssuerDirectoryDataSource(options, NullLogger.Instance); + _issuerRepository = new PostgresIssuerRepository(_dataSource, NullLogger.Instance); + _auditSink = new PostgresIssuerAuditSink(_dataSource, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await _fixture.TruncateAllTablesAsync(); + _issuerId = await SeedIssuerAsync(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task WriteAsync_PersistsAuditEntry() + { + var entry = CreateAuditEntry("issuer.created", "Issuer was created"); + + await _auditSink.WriteAsync(entry, CancellationToken.None); + + var persisted = await ReadAuditEntryAsync(entry.TenantId, entry.IssuerId); + persisted.Should().NotBeNull(); + persisted!.Action.Should().Be("issuer.created"); + persisted.Reason.Should().Be("Issuer was created"); + persisted.Actor.Should().Be("test@test.com"); + } + + [Fact] + public async Task WriteAsync_PersistsMetadata() + { + var metadata = new Dictionary + { + ["oldSlug"] = "old-issuer", + ["newSlug"] = "new-issuer" + }; + var entry = CreateAuditEntry("issuer.slug.changed", "Slug updated", metadata); + + await _auditSink.WriteAsync(entry, CancellationToken.None); + + var persisted = await ReadAuditEntryAsync(entry.TenantId, entry.IssuerId); + persisted.Should().NotBeNull(); + persisted!.Details.Should().ContainKey("oldSlug"); + persisted.Details["oldSlug"].Should().Be("old-issuer"); + persisted.Details.Should().ContainKey("newSlug"); + persisted.Details["newSlug"].Should().Be("new-issuer"); + } + + [Fact] + public async Task WriteAsync_PersistsEmptyMetadata() + { + var entry = CreateAuditEntry("issuer.deleted", "Issuer removed"); + + await _auditSink.WriteAsync(entry, CancellationToken.None); + + var persisted = await ReadAuditEntryAsync(entry.TenantId, entry.IssuerId); + persisted.Should().NotBeNull(); + persisted!.Details.Should().NotBeNull(); + persisted.Details.Should().BeEmpty(); + } + + [Fact] + public async Task WriteAsync_PersistsNullReason() + { + var entry = CreateAuditEntry("issuer.updated", null); + + await _auditSink.WriteAsync(entry, CancellationToken.None); + + var persisted = await ReadAuditEntryAsync(entry.TenantId, entry.IssuerId); + persisted.Should().NotBeNull(); + persisted!.Reason.Should().BeNull(); + } + + [Fact] + public async Task WriteAsync_PersistsTimestampCorrectly() + { + var now = DateTimeOffset.UtcNow; + var entry = CreateAuditEntry("issuer.key.added", "Key added", timestamp: now); + + await _auditSink.WriteAsync(entry, CancellationToken.None); + + var persisted = await ReadAuditEntryAsync(entry.TenantId, entry.IssuerId); + persisted.Should().NotBeNull(); + persisted!.OccurredAt.Should().BeCloseTo(now.UtcDateTime, TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task WriteAsync_PersistsMultipleEntriesForSameIssuer() + { + var entry1 = CreateAuditEntry("issuer.created", "Created"); + var entry2 = CreateAuditEntry("issuer.updated", "Updated"); + var entry3 = CreateAuditEntry("issuer.key.added", "Key added"); + + await _auditSink.WriteAsync(entry1, CancellationToken.None); + await _auditSink.WriteAsync(entry2, CancellationToken.None); + await _auditSink.WriteAsync(entry3, CancellationToken.None); + + var count = await CountAuditEntriesAsync(_tenantId, _issuerId); + count.Should().Be(3); + } + + [Fact] + public async Task WriteAsync_PersistsActorCorrectly() + { + var entry = new IssuerAuditEntry( + _tenantId, + _issuerId, + "issuer.trust.changed", + DateTimeOffset.UtcNow, + "admin@company.com", + "Trust level modified", + null); + + await _auditSink.WriteAsync(entry, CancellationToken.None); + + var persisted = await ReadAuditEntryAsync(entry.TenantId, entry.IssuerId); + persisted.Should().NotBeNull(); + persisted!.Actor.Should().Be("admin@company.com"); + } + + private async Task SeedIssuerAsync() + { + var issuerId = Guid.NewGuid().ToString(); + var now = DateTimeOffset.UtcNow; + var issuer = new IssuerRecord + { + Id = issuerId, + TenantId = _tenantId, + Slug = $"test-issuer-{Guid.NewGuid():N}", + DisplayName = "Test Issuer", + Description = "Test issuer for audit tests", + Contact = new IssuerContact(null, null, null, null), + Metadata = new IssuerMetadata(null, null, null, null, [], new Dictionary()), + Endpoints = [], + Tags = [], + IsSystemSeed = false, + CreatedAtUtc = now, + CreatedBy = "test@test.com", + UpdatedAtUtc = now, + UpdatedBy = "test@test.com" + }; + await _issuerRepository.UpsertAsync(issuer, CancellationToken.None); + return issuerId; + } + + private IssuerAuditEntry CreateAuditEntry( + string action, + string? reason, + IReadOnlyDictionary? metadata = null, + DateTimeOffset? timestamp = null) + { + return new IssuerAuditEntry( + _tenantId, + _issuerId, + action, + timestamp ?? DateTimeOffset.UtcNow, + "test@test.com", + reason, + metadata); + } + + private async Task ReadAuditEntryAsync(string tenantId, string issuerId) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", CancellationToken.None); + + const string sql = """ + SELECT actor, action, reason, details, occurred_at + FROM issuer.audit + WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid + ORDER BY occurred_at DESC + LIMIT 1 + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("tenantId", Guid.Parse(tenantId)); + command.Parameters.AddWithValue("issuerId", Guid.Parse(issuerId)); + + await using var reader = await command.ExecuteReaderAsync(); + if (!await reader.ReadAsync()) + { + return null; + } + + var detailsJson = reader.GetString(3); + var details = JsonSerializer.Deserialize>(detailsJson) ?? []; + + return new AuditEntryDto( + reader.GetString(0), + reader.GetString(1), + reader.IsDBNull(2) ? null : reader.GetString(2), + details, + reader.GetDateTime(4)); + } + + private async Task CountAuditEntriesAsync(string tenantId, string issuerId) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", CancellationToken.None); + + const string sql = """ + SELECT COUNT(*) + FROM issuer.audit + WHERE tenant_id = @tenantId::uuid AND issuer_id = @issuerId::uuid + """; + + await using var command = new NpgsqlCommand(sql, connection); + command.Parameters.AddWithValue("tenantId", Guid.Parse(tenantId)); + command.Parameters.AddWithValue("issuerId", Guid.Parse(issuerId)); + + var result = await command.ExecuteScalarAsync(); + return Convert.ToInt32(result); + } + + private sealed record AuditEntryDto( + string Actor, + string Action, + string? Reason, + Dictionary Details, + DateTime OccurredAt); +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerDirectoryPostgresFixture.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerDirectoryPostgresFixture.cs new file mode 100644 index 000000000..740723c40 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerDirectoryPostgresFixture.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using StellaOps.IssuerDirectory.Storage.Postgres; +using StellaOps.Infrastructure.Postgres.Testing; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +/// +/// PostgreSQL integration test fixture for the IssuerDirectory module. +/// Runs migrations from embedded resources and provides test isolation. +/// +public sealed class IssuerDirectoryPostgresFixture : PostgresIntegrationFixture, ICollectionFixture +{ + protected override Assembly? GetMigrationAssembly() + => typeof(IssuerDirectoryDataSource).Assembly; + + protected override string GetModuleName() => "IssuerDirectory"; +} + +/// +/// Collection definition for IssuerDirectory PostgreSQL integration tests. +/// Tests in this collection share a single PostgreSQL container instance. +/// +[CollectionDefinition(Name)] +public sealed class IssuerDirectoryPostgresCollection : ICollectionFixture +{ + public const string Name = "IssuerDirectoryPostgres"; +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerKeyRepositoryTests.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerKeyRepositoryTests.cs new file mode 100644 index 000000000..ac072e6ce --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerKeyRepositoryTests.cs @@ -0,0 +1,288 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Infrastructure.Postgres.Options; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +[Collection(IssuerDirectoryPostgresCollection.Name)] +public sealed class IssuerKeyRepositoryTests : IAsyncLifetime +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + private readonly PostgresIssuerRepository _issuerRepository; + private readonly PostgresIssuerKeyRepository _keyRepository; + private readonly string _tenantId = Guid.NewGuid().ToString(); + private string _issuerId = null!; + + public IssuerKeyRepositoryTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + + var options = new PostgresOptions + { + ConnectionString = fixture.ConnectionString, + SchemaName = fixture.SchemaName + }; + var dataSource = new IssuerDirectoryDataSource(options, NullLogger.Instance); + _issuerRepository = new PostgresIssuerRepository(dataSource, NullLogger.Instance); + _keyRepository = new PostgresIssuerKeyRepository(dataSource, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await _fixture.TruncateAllTablesAsync(); + _issuerId = await SeedIssuerAsync(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task UpsertAsync_CreatesNewKey() + { + var keyRecord = CreateKeyRecord("key-001", IssuerKeyType.Ed25519PublicKey); + + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-001", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Id.Should().Be("key-001"); + fetched.Type.Should().Be(IssuerKeyType.Ed25519PublicKey); + fetched.Status.Should().Be(IssuerKeyStatus.Active); + } + + [Fact] + public async Task UpsertAsync_UpdatesExistingKey() + { + var keyRecord = CreateKeyRecord("key-update", IssuerKeyType.Ed25519PublicKey); + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var updated = keyRecord with + { + Status = IssuerKeyStatus.Retired, + RetiredAtUtc = DateTimeOffset.UtcNow, + UpdatedAtUtc = DateTimeOffset.UtcNow, + UpdatedBy = "admin@test.com" + }; + + await _keyRepository.UpsertAsync(updated, CancellationToken.None); + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-update", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Status.Should().Be(IssuerKeyStatus.Retired); + fetched.RetiredAtUtc.Should().NotBeNull(); + } + + [Fact] + public async Task GetAsync_ReturnsNullForNonExistentKey() + { + var result = await _keyRepository.GetAsync(_tenantId, _issuerId, "nonexistent", CancellationToken.None); + + result.Should().BeNull(); + } + + [Fact] + public async Task GetByFingerprintAsync_ReturnsKey() + { + var fingerprint = $"fp_{Guid.NewGuid():N}"; + var keyRecord = CreateKeyRecord("key-fp", IssuerKeyType.Ed25519PublicKey) with { Fingerprint = fingerprint }; + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var fetched = await _keyRepository.GetByFingerprintAsync(_tenantId, _issuerId, fingerprint, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Fingerprint.Should().Be(fingerprint); + } + + [Fact] + public async Task ListAsync_ReturnsAllKeysForIssuer() + { + var key1 = CreateKeyRecord("key-list-1", IssuerKeyType.Ed25519PublicKey); + var key2 = CreateKeyRecord("key-list-2", IssuerKeyType.X509Certificate); + + await _keyRepository.UpsertAsync(key1, CancellationToken.None); + await _keyRepository.UpsertAsync(key2, CancellationToken.None); + + var results = await _keyRepository.ListAsync(_tenantId, _issuerId, CancellationToken.None); + + results.Should().HaveCount(2); + results.Select(k => k.Id).Should().BeEquivalentTo(["key-list-1", "key-list-2"]); + } + + [Fact] + public async Task ListGlobalAsync_ReturnsGlobalKeys() + { + var globalIssuerId = await SeedGlobalIssuerAsync(); + var globalKey = CreateKeyRecord("global-key", IssuerKeyType.Ed25519PublicKey, globalIssuerId, IssuerTenants.Global); + await _keyRepository.UpsertAsync(globalKey, CancellationToken.None); + + var results = await _keyRepository.ListGlobalAsync(globalIssuerId, CancellationToken.None); + + results.Should().Contain(k => k.Id == "global-key"); + } + + [Fact] + public async Task UpsertAsync_PersistsKeyTypeEd25519() + { + var keyRecord = CreateKeyRecord("key-ed25519", IssuerKeyType.Ed25519PublicKey); + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-ed25519", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Type.Should().Be(IssuerKeyType.Ed25519PublicKey); + } + + [Fact] + public async Task UpsertAsync_PersistsKeyTypeX509() + { + var keyRecord = CreateKeyRecord("key-x509", IssuerKeyType.X509Certificate); + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-x509", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Type.Should().Be(IssuerKeyType.X509Certificate); + } + + [Fact] + public async Task UpsertAsync_PersistsKeyTypeDsse() + { + var keyRecord = CreateKeyRecord("key-dsse", IssuerKeyType.DssePublicKey); + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-dsse", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Type.Should().Be(IssuerKeyType.DssePublicKey); + } + + [Fact] + public async Task UpsertAsync_PersistsRevokedStatus() + { + var keyRecord = CreateKeyRecord("key-revoked", IssuerKeyType.Ed25519PublicKey) with + { + Status = IssuerKeyStatus.Revoked, + RevokedAtUtc = DateTimeOffset.UtcNow + }; + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-revoked", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Status.Should().Be(IssuerKeyStatus.Revoked); + fetched.RevokedAtUtc.Should().NotBeNull(); + } + + [Fact] + public async Task UpsertAsync_PersistsReplacesKeyId() + { + var oldKey = CreateKeyRecord("old-key", IssuerKeyType.Ed25519PublicKey) with + { + Status = IssuerKeyStatus.Retired, + RetiredAtUtc = DateTimeOffset.UtcNow + }; + await _keyRepository.UpsertAsync(oldKey, CancellationToken.None); + + var newKey = CreateKeyRecord("new-key", IssuerKeyType.Ed25519PublicKey) with + { + ReplacesKeyId = "old-key" + }; + await _keyRepository.UpsertAsync(newKey, CancellationToken.None); + + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "new-key", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.ReplacesKeyId.Should().Be("old-key"); + } + + [Fact] + public async Task UpsertAsync_PersistsExpirationDate() + { + var expiresAt = DateTimeOffset.UtcNow.AddYears(1); + var keyRecord = CreateKeyRecord("key-expires", IssuerKeyType.Ed25519PublicKey) with + { + ExpiresAtUtc = expiresAt + }; + await _keyRepository.UpsertAsync(keyRecord, CancellationToken.None); + + var fetched = await _keyRepository.GetAsync(_tenantId, _issuerId, "key-expires", CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.ExpiresAtUtc.Should().BeCloseTo(expiresAt, TimeSpan.FromSeconds(1)); + } + + private async Task SeedIssuerAsync() + { + var issuerId = Guid.NewGuid().ToString(); + var now = DateTimeOffset.UtcNow; + var issuer = new IssuerRecord + { + Id = issuerId, + TenantId = _tenantId, + Slug = $"test-issuer-{Guid.NewGuid():N}", + DisplayName = "Test Issuer", + Description = "Test issuer for key tests", + Contact = new IssuerContact(null, null, null, null), + Metadata = new IssuerMetadata(null, null, null, null, [], new Dictionary()), + Endpoints = [], + Tags = [], + IsSystemSeed = false, + CreatedAtUtc = now, + CreatedBy = "test@test.com", + UpdatedAtUtc = now, + UpdatedBy = "test@test.com" + }; + await _issuerRepository.UpsertAsync(issuer, CancellationToken.None); + return issuerId; + } + + private async Task SeedGlobalIssuerAsync() + { + var issuerId = Guid.NewGuid().ToString(); + var now = DateTimeOffset.UtcNow; + var issuer = new IssuerRecord + { + Id = issuerId, + TenantId = IssuerTenants.Global, + Slug = $"global-issuer-{Guid.NewGuid():N}", + DisplayName = "Global Test Issuer", + Description = "Global test issuer", + Contact = new IssuerContact(null, null, null, null), + Metadata = new IssuerMetadata(null, null, null, null, [], new Dictionary()), + Endpoints = [], + Tags = [], + IsSystemSeed = true, + CreatedAtUtc = now, + CreatedBy = "system", + UpdatedAtUtc = now, + UpdatedBy = "system" + }; + await _issuerRepository.UpsertAsync(issuer, CancellationToken.None); + return issuerId; + } + + private IssuerKeyRecord CreateKeyRecord(string keyId, IssuerKeyType type, string? issuerId = null, string? tenantId = null) + { + var now = DateTimeOffset.UtcNow; + return new IssuerKeyRecord + { + Id = keyId, + IssuerId = issuerId ?? _issuerId, + TenantId = tenantId ?? _tenantId, + Type = type, + Status = IssuerKeyStatus.Active, + Material = new IssuerKeyMaterial("pem", $"-----BEGIN PUBLIC KEY-----\nMFkwE...\n-----END PUBLIC KEY-----"), + Fingerprint = $"fp_{Guid.NewGuid():N}", + CreatedAtUtc = now, + CreatedBy = "test@test.com", + UpdatedAtUtc = now, + UpdatedBy = "test@test.com", + ExpiresAtUtc = null, + RetiredAtUtc = null, + RevokedAtUtc = null, + ReplacesKeyId = null + }; + } +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerRepositoryTests.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerRepositoryTests.cs new file mode 100644 index 000000000..bb02366f3 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerRepositoryTests.cs @@ -0,0 +1,220 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Infrastructure.Postgres.Options; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +[Collection(IssuerDirectoryPostgresCollection.Name)] +public sealed class IssuerRepositoryTests : IAsyncLifetime +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + private readonly PostgresIssuerRepository _repository; + private readonly string _tenantId = Guid.NewGuid().ToString(); + + public IssuerRepositoryTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + + var options = new PostgresOptions + { + ConnectionString = fixture.ConnectionString, + SchemaName = fixture.SchemaName + }; + var dataSource = new IssuerDirectoryDataSource(options, NullLogger.Instance); + _repository = new PostgresIssuerRepository(dataSource, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await _fixture.TruncateAllTablesAsync(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task UpsertAsync_CreatesNewIssuer() + { + var record = CreateIssuerRecord("test-issuer", "Test Issuer"); + + await _repository.UpsertAsync(record, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Id.Should().Be(record.Id); + fetched.Slug.Should().Be("test-issuer"); + fetched.DisplayName.Should().Be("Test Issuer"); + fetched.TenantId.Should().Be(_tenantId); + } + + [Fact] + public async Task UpsertAsync_UpdatesExistingIssuer() + { + var record = CreateIssuerRecord("update-test", "Original Name"); + await _repository.UpsertAsync(record, CancellationToken.None); + + var updated = record with + { + DisplayName = "Updated Name", + Description = "Updated description", + UpdatedAtUtc = DateTimeOffset.UtcNow, + UpdatedBy = "updater@test.com" + }; + + await _repository.UpsertAsync(updated, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.DisplayName.Should().Be("Updated Name"); + fetched.Description.Should().Be("Updated description"); + } + + [Fact] + public async Task GetAsync_ReturnsNullForNonExistentIssuer() + { + var result = await _repository.GetAsync(_tenantId, Guid.NewGuid().ToString(), CancellationToken.None); + + result.Should().BeNull(); + } + + [Fact] + public async Task ListAsync_ReturnsAllIssuersForTenant() + { + var issuer1 = CreateIssuerRecord("issuer-a", "Issuer A"); + var issuer2 = CreateIssuerRecord("issuer-b", "Issuer B"); + + await _repository.UpsertAsync(issuer1, CancellationToken.None); + await _repository.UpsertAsync(issuer2, CancellationToken.None); + + var results = await _repository.ListAsync(_tenantId, CancellationToken.None); + + results.Should().HaveCount(2); + results.Select(i => i.Slug).Should().BeEquivalentTo(["issuer-a", "issuer-b"]); + } + + [Fact] + public async Task ListGlobalAsync_ReturnsGlobalIssuers() + { + var globalIssuer = CreateIssuerRecord("global-issuer", "Global Issuer", IssuerTenants.Global); + await _repository.UpsertAsync(globalIssuer, CancellationToken.None); + + var results = await _repository.ListGlobalAsync(CancellationToken.None); + + results.Should().Contain(i => i.Slug == "global-issuer"); + } + + [Fact] + public async Task DeleteAsync_RemovesIssuer() + { + var record = CreateIssuerRecord("to-delete", "To Delete"); + await _repository.UpsertAsync(record, CancellationToken.None); + + await _repository.DeleteAsync(_tenantId, record.Id, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().BeNull(); + } + + [Fact] + public async Task UpsertAsync_PersistsContactInformation() + { + var contact = new IssuerContact( + "security@example.com", + "+1-555-0100", + new Uri("https://example.com/security"), + "UTC"); + + var record = CreateIssuerRecord("contact-test", "Contact Test") with { Contact = contact }; + + await _repository.UpsertAsync(record, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Contact.Email.Should().Be("security@example.com"); + fetched.Contact.Phone.Should().Be("+1-555-0100"); + fetched.Contact.Website.Should().Be(new Uri("https://example.com/security")); + fetched.Contact.Timezone.Should().Be("UTC"); + } + + [Fact] + public async Task UpsertAsync_PersistsEndpoints() + { + var endpoints = new List + { + new("csaf", new Uri("https://example.com/.well-known/csaf/provider-metadata.json"), "json", false), + new("oidc", new Uri("https://example.com/.well-known/openid-configuration"), "json", true) + }; + + var record = CreateIssuerRecord("endpoints-test", "Endpoints Test") with { Endpoints = endpoints }; + + await _repository.UpsertAsync(record, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Endpoints.Should().HaveCount(2); + fetched.Endpoints.Should().Contain(e => e.Kind == "csaf"); + fetched.Endpoints.Should().Contain(e => e.Kind == "oidc" && e.RequiresAuthentication); + } + + [Fact] + public async Task UpsertAsync_PersistsMetadata() + { + var metadata = new IssuerMetadata( + "CVE-2024-0001", + "csaf-pub-123", + new Uri("https://example.com/security-advisories"), + new Uri("https://example.com/catalog"), + ["en", "de"], + new Dictionary { ["custom"] = "value" }); + + var record = CreateIssuerRecord("metadata-test", "Metadata Test") with { Metadata = metadata }; + + await _repository.UpsertAsync(record, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Metadata.CveOrgId.Should().Be("CVE-2024-0001"); + fetched.Metadata.CsafPublisherId.Should().Be("csaf-pub-123"); + fetched.Metadata.SupportedLanguages.Should().BeEquivalentTo(["en", "de"]); + fetched.Metadata.Attributes.Should().ContainKey("custom"); + } + + [Fact] + public async Task UpsertAsync_PersistsTags() + { + var record = CreateIssuerRecord("tags-test", "Tags Test") with + { + Tags = ["vendor", "upstream", "critical"] + }; + + await _repository.UpsertAsync(record, CancellationToken.None); + var fetched = await _repository.GetAsync(_tenantId, record.Id, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Tags.Should().BeEquivalentTo(["vendor", "upstream", "critical"]); + } + + private IssuerRecord CreateIssuerRecord(string slug, string displayName, string? tenantId = null) + { + var now = DateTimeOffset.UtcNow; + return new IssuerRecord + { + Id = Guid.NewGuid().ToString(), + TenantId = tenantId ?? _tenantId, + Slug = slug, + DisplayName = displayName, + Description = $"Test issuer: {displayName}", + Contact = new IssuerContact(null, null, null, null), + Metadata = new IssuerMetadata(null, null, null, null, [], new Dictionary()), + Endpoints = [], + Tags = [], + IsSystemSeed = false, + CreatedAtUtc = now, + CreatedBy = "test@test.com", + UpdatedAtUtc = now, + UpdatedBy = "test@test.com" + }; + } +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerTrustRepositoryTests.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerTrustRepositoryTests.cs new file mode 100644 index 000000000..3b9c5ac96 --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerTrustRepositoryTests.cs @@ -0,0 +1,190 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Infrastructure.Postgres.Options; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +[Collection(IssuerDirectoryPostgresCollection.Name)] +public sealed class IssuerTrustRepositoryTests : IAsyncLifetime +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + private readonly PostgresIssuerRepository _issuerRepository; + private readonly PostgresIssuerTrustRepository _trustRepository; + private readonly string _tenantId = Guid.NewGuid().ToString(); + private string _issuerId = null!; + + public IssuerTrustRepositoryTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + + var options = new PostgresOptions + { + ConnectionString = fixture.ConnectionString, + SchemaName = fixture.SchemaName + }; + var dataSource = new IssuerDirectoryDataSource(options, NullLogger.Instance); + _issuerRepository = new PostgresIssuerRepository(dataSource, NullLogger.Instance); + _trustRepository = new PostgresIssuerTrustRepository(dataSource, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await _fixture.TruncateAllTablesAsync(); + _issuerId = await SeedIssuerAsync(); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task UpsertAsync_CreatesNewTrustOverride() + { + var record = CreateTrustRecord(5.5m, "Trusted vendor"); + + await _trustRepository.UpsertAsync(record, CancellationToken.None); + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Weight.Should().Be(5.5m); + fetched.Reason.Should().Be("Trusted vendor"); + } + + [Fact] + public async Task UpsertAsync_UpdatesExistingTrustOverride() + { + var record = CreateTrustRecord(3.0m, "Initial trust"); + await _trustRepository.UpsertAsync(record, CancellationToken.None); + + var updated = record.WithUpdated(7.5m, "Upgraded trust", DateTimeOffset.UtcNow, "admin@test.com"); + await _trustRepository.UpsertAsync(updated, CancellationToken.None); + + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Weight.Should().Be(7.5m); + fetched.Reason.Should().Be("Upgraded trust"); + fetched.UpdatedBy.Should().Be("admin@test.com"); + } + + [Fact] + public async Task GetAsync_ReturnsNullForNonExistentOverride() + { + var result = await _trustRepository.GetAsync(_tenantId, Guid.NewGuid().ToString(), CancellationToken.None); + + result.Should().BeNull(); + } + + [Fact] + public async Task DeleteAsync_RemovesTrustOverride() + { + var record = CreateTrustRecord(2.0m, "To be deleted"); + await _trustRepository.UpsertAsync(record, CancellationToken.None); + + await _trustRepository.DeleteAsync(_tenantId, _issuerId, CancellationToken.None); + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().BeNull(); + } + + [Fact] + public async Task UpsertAsync_PersistsPositiveWeight() + { + var record = CreateTrustRecord(10.0m, "Maximum trust"); + await _trustRepository.UpsertAsync(record, CancellationToken.None); + + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Weight.Should().Be(10.0m); + } + + [Fact] + public async Task UpsertAsync_PersistsNegativeWeight() + { + var record = CreateTrustRecord(-5.0m, "Distrust override"); + await _trustRepository.UpsertAsync(record, CancellationToken.None); + + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Weight.Should().Be(-5.0m); + } + + [Fact] + public async Task UpsertAsync_PersistsZeroWeight() + { + var record = CreateTrustRecord(0m, "Neutral trust"); + await _trustRepository.UpsertAsync(record, CancellationToken.None); + + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Weight.Should().Be(0m); + } + + [Fact] + public async Task UpsertAsync_PersistsNullReason() + { + var record = CreateTrustRecord(5.0m, null); + await _trustRepository.UpsertAsync(record, CancellationToken.None); + + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Reason.Should().BeNull(); + } + + [Fact] + public async Task UpsertAsync_PersistsTimestamps() + { + var now = DateTimeOffset.UtcNow; + var record = CreateTrustRecord(5.0m, "Time test"); + + await _trustRepository.UpsertAsync(record, CancellationToken.None); + var fetched = await _trustRepository.GetAsync(_tenantId, _issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.CreatedAtUtc.Should().BeCloseTo(now, TimeSpan.FromSeconds(5)); + fetched.UpdatedAtUtc.Should().BeCloseTo(now, TimeSpan.FromSeconds(5)); + fetched.CreatedBy.Should().Be("test@test.com"); + fetched.UpdatedBy.Should().Be("test@test.com"); + } + + private async Task SeedIssuerAsync() + { + var issuerId = Guid.NewGuid().ToString(); + var now = DateTimeOffset.UtcNow; + var issuer = new IssuerRecord + { + Id = issuerId, + TenantId = _tenantId, + Slug = $"test-issuer-{Guid.NewGuid():N}", + DisplayName = "Test Issuer", + Description = "Test issuer for trust tests", + Contact = new IssuerContact(null, null, null, null), + Metadata = new IssuerMetadata(null, null, null, null, [], new Dictionary()), + Endpoints = [], + Tags = [], + IsSystemSeed = false, + CreatedAtUtc = now, + CreatedBy = "test@test.com", + UpdatedAtUtc = now, + UpdatedBy = "test@test.com" + }; + await _issuerRepository.UpsertAsync(issuer, CancellationToken.None); + return issuerId; + } + + private IssuerTrustOverrideRecord CreateTrustRecord(decimal weight, string? reason) + { + return IssuerTrustOverrideRecord.Create( + _issuerId, + _tenantId, + weight, + reason, + DateTimeOffset.UtcNow, + "test@test.com"); + } +} diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests.csproj b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests.csproj new file mode 100644 index 000000000..5651443af --- /dev/null +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests.csproj @@ -0,0 +1,37 @@ + + + + + net10.0 + enable + enable + preview + false + true + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerDirectoryPostgresFixture.cs b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerDirectoryPostgresFixture.cs new file mode 100644 index 000000000..d44c91714 --- /dev/null +++ b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerDirectoryPostgresFixture.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using Microsoft.Extensions.Logging; +using StellaOps.Infrastructure.Postgres.Testing; +using StellaOps.IssuerDirectory.Storage.Postgres; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +public sealed class IssuerDirectoryPostgresFixture : PostgresIntegrationFixture +{ + protected override Assembly? GetMigrationAssembly() => typeof(IssuerDirectoryDataSource).Assembly; + protected override string GetModuleName() => "issuer"; + protected override string? GetResourcePrefix() => "IssuerDirectory.Storage.Postgres.Migrations"; + protected override ILogger Logger => Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; +} diff --git a/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerKeyRepositoryTests.cs b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerKeyRepositoryTests.cs new file mode 100644 index 000000000..c9b1bacfd --- /dev/null +++ b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerKeyRepositoryTests.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +public class IssuerKeyRepositoryTests : IClassFixture +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + + public IssuerKeyRepositoryTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + } + + private PostgresIssuerRepository CreateIssuerRepo() => + new(new IssuerDirectoryDataSource(_fixture.Fixture.Options, NullLogger.Instance), + NullLogger.Instance); + + private PostgresIssuerKeyRepository CreateKeyRepo() => + new(new IssuerDirectoryDataSource(_fixture.Fixture.Options, NullLogger.Instance), + NullLogger.Instance); + + [Fact] + public async Task AddKey_And_List_Works() + { + var tenant = Guid.NewGuid().ToString(); + var issuerId = Guid.NewGuid().ToString(); + var issuerRepo = CreateIssuerRepo(); + var keyRepo = CreateKeyRepo(); + + var issuer = new IssuerRecord( + issuerId, + tenant, + slug: "vendor-x", + displayName: "Vendor X", + description: null, + endpoints: Array.Empty(), + contact: new IssuerContact(null, null), + metadata: new IssuerMetadata(Array.Empty(), null), + tags: Array.Empty(), + status: "active", + isSystemSeed: false, + createdAt: DateTimeOffset.UtcNow, + createdBy: "test", + updatedAt: DateTimeOffset.UtcNow, + updatedBy: "test"); + await issuerRepo.UpsertAsync(issuer, CancellationToken.None); + + var key = new IssuerKeyRecord( + id: Guid.NewGuid().ToString(), + issuerId: issuerId, + keyId: "kid-1", + keyType: IssuerKeyType.Ed25519, + publicKey: "pubkey", + fingerprint: "fp-1", + notBefore: null, + notAfter: null, + status: IssuerKeyStatus.Active, + createdAt: DateTimeOffset.UtcNow, + createdBy: "test", + revokedAt: null, + revokedBy: null, + revokeReason: null, + metadata: new IssuerKeyMetadata(null, null)); + + await keyRepo.UpsertAsync(key, CancellationToken.None); + + var keys = await keyRepo.ListAsync(tenant, issuerId, CancellationToken.None); + keys.Should().ContainSingle(k => k.KeyId == "kid-1" && k.IssuerId == issuerId); + } +} diff --git a/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerRepositoryTests.cs b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerRepositoryTests.cs new file mode 100644 index 000000000..c62a3469c --- /dev/null +++ b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/IssuerRepositoryTests.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +public class IssuerRepositoryTests : IClassFixture +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + + public IssuerRepositoryTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + } + + private PostgresIssuerRepository CreateRepository() + { + var dataSource = new IssuerDirectoryDataSource( + _fixture.Fixture.Options, + NullLogger.Instance); + return new PostgresIssuerRepository(dataSource, NullLogger.Instance); + } + + [Fact] + public async Task UpsertAndGet_Works_For_Tenant() + { + var repo = CreateRepository(); + var tenant = Guid.NewGuid().ToString(); + var issuerId = Guid.NewGuid().ToString(); + var record = new IssuerRecord( + issuerId, + tenant, + slug: "acme", + displayName: "Acme Corp", + description: "Test issuer", + endpoints: new[] { new IssuerEndpoint("csaf", "https://acme.test/csaf") }, + contact: new IssuerContact("security@acme.test", null), + metadata: new IssuerMetadata(Array.Empty(), null), + tags: new[] { "vendor", "csaf" }, + status: "active", + isSystemSeed: false, + createdAt: DateTimeOffset.UtcNow, + createdBy: "test", + updatedAt: DateTimeOffset.UtcNow, + updatedBy: "test"); + + await repo.UpsertAsync(record, CancellationToken.None); + + var fetched = await repo.GetAsync(tenant, issuerId, CancellationToken.None); + + fetched.Should().NotBeNull(); + fetched!.Slug.Should().Be("acme"); + fetched.DisplayName.Should().Be("Acme Corp"); + fetched.Endpoints.Should().HaveCount(1); + } +} diff --git a/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests.csproj b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests.csproj new file mode 100644 index 000000000..60371c01f --- /dev/null +++ b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests.csproj @@ -0,0 +1,28 @@ + + + net10.0 + enable + enable + preview + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/TrustRepositoryTests.cs b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/TrustRepositoryTests.cs new file mode 100644 index 000000000..15defb69e --- /dev/null +++ b/src/IssuerDirectory/__Tests/StellaOps.IssuerDirectory.Storage.Postgres.Tests/TrustRepositoryTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.IssuerDirectory.Core.Domain; +using StellaOps.IssuerDirectory.Storage.Postgres; +using StellaOps.IssuerDirectory.Storage.Postgres.Repositories; +using Xunit; + +namespace StellaOps.IssuerDirectory.Storage.Postgres.Tests; + +public class TrustRepositoryTests : IClassFixture +{ + private readonly IssuerDirectoryPostgresFixture _fixture; + + public TrustRepositoryTests(IssuerDirectoryPostgresFixture fixture) + { + _fixture = fixture; + } + + private PostgresIssuerRepository CreateIssuerRepo() => + new(new IssuerDirectoryDataSource(_fixture.Fixture.Options, NullLogger.Instance), + NullLogger.Instance); + + private PostgresIssuerTrustRepository CreateTrustRepo() => + new(new IssuerDirectoryDataSource(_fixture.Fixture.Options, NullLogger.Instance), + NullLogger.Instance); + + [Fact] + public async Task UpsertTrustOverride_Works() + { + var tenant = Guid.NewGuid().ToString(); + var issuerId = Guid.NewGuid().ToString(); + var issuerRepo = CreateIssuerRepo(); + var trustRepo = CreateTrustRepo(); + + var issuer = new IssuerRecord( + issuerId, + tenant, + slug: "trusty", + displayName: "Trusty Issuer", + description: null, + endpoints: Array.Empty(), + contact: new IssuerContact(null, null), + metadata: new IssuerMetadata(Array.Empty(), null), + tags: Array.Empty(), + status: "active", + isSystemSeed: false, + createdAt: DateTimeOffset.UtcNow, + createdBy: "test", + updatedAt: DateTimeOffset.UtcNow, + updatedBy: "test"); + await issuerRepo.UpsertAsync(issuer, CancellationToken.None); + + var trust = new IssuerTrustOverrideRecord( + id: Guid.NewGuid().ToString(), + issuerId: issuerId, + tenantId: tenant, + weight: 0.75m, + rationale: "vendor override", + expiresAt: null, + createdAt: DateTimeOffset.UtcNow, + createdBy: "test", + updatedAt: DateTimeOffset.UtcNow, + updatedBy: "test"); + + await trustRepo.UpsertAsync(trust, CancellationToken.None); + + var fetched = await trustRepo.GetAsync(tenant, issuerId, CancellationToken.None); + fetched.Should().NotBeNull(); + fetched!.Weight.Should().Be(0.75m); + } +} diff --git a/src/TaskRunner/StellaOps.TaskRunner/TASKS.md b/src/TaskRunner/StellaOps.TaskRunner/TASKS.md index 783f2fc2c..1890f2f17 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/TASKS.md +++ b/src/TaskRunner/StellaOps.TaskRunner/TASKS.md @@ -16,5 +16,6 @@ | TASKRUN-OBS-51-001 | DONE (2025-11-25) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-OBS-50-001 | Metrics/SLOs; depends on 50-001. | | TASKRUN-OBS-52-001 | BLOCKED (2025-11-25) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-OBS-51-001 | Timeline events; blocked: schema/evidence-pointer contract not published. | | TASKRUN-OBS-53-001 | BLOCKED (2025-11-25) | SPRINT_0157_0001_0001_taskrunner_i | TASKRUN-OBS-52-001 | Evidence locker snapshots; blocked: waiting on timeline schema/pointer contract. | +| TASKRUN-GAPS-157-014 | DONE (2025-12-05) | SPRINT_0157_0001_0001_taskrunner_i | — | TP1–TP10 remediation: canonical plan-hash recipe, inputs.lock evidence, approval DSSE ledger, redaction, deterministic RNG/time, sandbox/egress quotas, registry signing + SBOM + revocation, offline bundle schema + verifier script, SLO/alerting, fail-closed gates. | Status source of truth: `docs/implplan/SPRINT_0157_0001_0001_taskrunner_i.md`. Update both files together. Keep UTC dates when advancing status. diff --git a/src/__Libraries/__Tests/StellaOps.Microservice.Tests/EndpointRegistryTests.cs b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/EndpointRegistryTests.cs new file mode 100644 index 000000000..9b7ab4965 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/EndpointRegistryTests.cs @@ -0,0 +1,376 @@ +using StellaOps.Router.Common.Models; + +namespace StellaOps.Microservice.Tests; + +/// +/// Unit tests for . +/// +public sealed class EndpointRegistryTests +{ + private static EndpointDescriptor CreateEndpoint(string method, string path) + { + return new EndpointDescriptor + { + ServiceName = "test-service", + Version = "1.0.0", + Method = method, + Path = path + }; + } + + #region Register Tests + + [Fact] + public void Register_SingleEndpoint_AddsToRegistry() + { + // Arrange + var registry = new EndpointRegistry(); + var endpoint = CreateEndpoint("GET", "/api/users"); + + // Act + registry.Register(endpoint); + + // Assert + registry.GetAllEndpoints().Should().HaveCount(1); + registry.GetAllEndpoints()[0].Should().Be(endpoint); + } + + [Fact] + public void Register_MultipleEndpoints_AddsAllToRegistry() + { + // Arrange + var registry = new EndpointRegistry(); + + // Act + registry.Register(CreateEndpoint("GET", "/api/users")); + registry.Register(CreateEndpoint("POST", "/api/users")); + registry.Register(CreateEndpoint("GET", "/api/users/{id}")); + + // Assert + registry.GetAllEndpoints().Should().HaveCount(3); + } + + [Fact] + public void RegisterAll_AddsAllEndpoints() + { + // Arrange + var registry = new EndpointRegistry(); + var endpoints = new[] + { + CreateEndpoint("GET", "/api/users"), + CreateEndpoint("POST", "/api/users"), + CreateEndpoint("DELETE", "/api/users/{id}") + }; + + // Act + registry.RegisterAll(endpoints); + + // Assert + registry.GetAllEndpoints().Should().HaveCount(3); + } + + [Fact] + public void RegisterAll_WithEmptyCollection_DoesNotAddAny() + { + // Arrange + var registry = new EndpointRegistry(); + + // Act + registry.RegisterAll([]); + + // Assert + registry.GetAllEndpoints().Should().BeEmpty(); + } + + #endregion + + #region TryMatch Method Tests + + [Fact] + public void TryMatch_ExactMethodAndPath_ReturnsTrue() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users")); + + // Act + var result = registry.TryMatch("GET", "/api/users", out var match); + + // Assert + result.Should().BeTrue(); + match.Should().NotBeNull(); + match!.Endpoint.Path.Should().Be("/api/users"); + } + + [Fact] + public void TryMatch_NonMatchingMethod_ReturnsFalse() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users")); + + // Act + var result = registry.TryMatch("POST", "/api/users", out var match); + + // Assert + result.Should().BeFalse(); + match.Should().BeNull(); + } + + [Fact] + public void TryMatch_NonMatchingPath_ReturnsFalse() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users")); + + // Act + var result = registry.TryMatch("GET", "/api/items", out var match); + + // Assert + result.Should().BeFalse(); + match.Should().BeNull(); + } + + [Fact] + public void TryMatch_MethodIsCaseInsensitive() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users")); + + // Act & Assert + registry.TryMatch("get", "/api/users", out _).Should().BeTrue(); + registry.TryMatch("Get", "/api/users", out _).Should().BeTrue(); + registry.TryMatch("GET", "/api/users", out _).Should().BeTrue(); + } + + [Fact] + public void TryMatch_PathIsCaseInsensitive_WhenEnabled() + { + // Arrange + var registry = new EndpointRegistry(caseInsensitive: true); + registry.Register(CreateEndpoint("GET", "/api/users")); + + // Act & Assert + registry.TryMatch("GET", "/API/USERS", out _).Should().BeTrue(); + registry.TryMatch("GET", "/Api/Users", out _).Should().BeTrue(); + } + + [Fact] + public void TryMatch_PathIsCaseSensitive_WhenDisabled() + { + // Arrange + var registry = new EndpointRegistry(caseInsensitive: false); + registry.Register(CreateEndpoint("GET", "/api/users")); + + // Act & Assert + registry.TryMatch("GET", "/api/users", out _).Should().BeTrue(); + registry.TryMatch("GET", "/API/USERS", out _).Should().BeFalse(); + } + + #endregion + + #region TryMatch Path Parameter Tests + + [Fact] + public void TryMatch_PathWithParameter_ExtractsParameter() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users/{id}")); + + // Act + var result = registry.TryMatch("GET", "/api/users/123", out var match); + + // Assert + result.Should().BeTrue(); + match.Should().NotBeNull(); + match!.PathParameters.Should().ContainKey("id"); + match.PathParameters["id"].Should().Be("123"); + } + + [Fact] + public void TryMatch_PathWithMultipleParameters_ExtractsAll() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users/{userId}/orders/{orderId}")); + + // Act + var result = registry.TryMatch("GET", "/api/users/456/orders/789", out var match); + + // Assert + result.Should().BeTrue(); + match.Should().NotBeNull(); + match!.PathParameters.Should().HaveCount(2); + match.PathParameters["userId"].Should().Be("456"); + match.PathParameters["orderId"].Should().Be("789"); + } + + [Fact] + public void TryMatch_PathParameterWithSpecialChars_ExtractsParameter() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/items/{itemId}")); + + // Act + var result = registry.TryMatch("GET", "/api/items/item-with-dashes", out var match); + + // Assert + result.Should().BeTrue(); + match!.PathParameters["itemId"].Should().Be("item-with-dashes"); + } + + [Fact] + public void TryMatch_EmptyPathParameter_DoesNotMatch() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users/{id}")); + + // Act + var result = registry.TryMatch("GET", "/api/users/", out var match); + + // Assert + result.Should().BeFalse(); + } + + #endregion + + #region TryMatch Multiple Endpoints Tests + + [Fact] + public void TryMatch_FirstMatchingEndpoint_ReturnsFirst() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/users")); + registry.Register(CreateEndpoint("GET", "/api/users")); // duplicate + + // Act + registry.TryMatch("GET", "/api/users", out var match); + + // Assert - should return the first registered + match.Should().NotBeNull(); + } + + [Fact] + public void TryMatch_SelectsCorrectEndpointByMethod() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(new EndpointDescriptor + { + ServiceName = "test", + Version = "1.0", + Method = "GET", + Path = "/api/users", + DefaultTimeout = TimeSpan.FromSeconds(10) + }); + registry.Register(new EndpointDescriptor + { + ServiceName = "test", + Version = "1.0", + Method = "POST", + Path = "/api/users", + DefaultTimeout = TimeSpan.FromSeconds(30) + }); + + // Act + registry.TryMatch("POST", "/api/users", out var match); + + // Assert + match.Should().NotBeNull(); + match!.Endpoint.Method.Should().Be("POST"); + match.Endpoint.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); + } + + #endregion + + #region GetAllEndpoints Tests + + [Fact] + public void GetAllEndpoints_EmptyRegistry_ReturnsEmptyList() + { + // Arrange + var registry = new EndpointRegistry(); + + // Act + var endpoints = registry.GetAllEndpoints(); + + // Assert + endpoints.Should().BeEmpty(); + } + + [Fact] + public void GetAllEndpoints_ReturnsAllRegisteredEndpoints() + { + // Arrange + var registry = new EndpointRegistry(); + var endpoint1 = CreateEndpoint("GET", "/api/a"); + var endpoint2 = CreateEndpoint("POST", "/api/b"); + var endpoint3 = CreateEndpoint("DELETE", "/api/c"); + registry.RegisterAll([endpoint1, endpoint2, endpoint3]); + + // Act + var endpoints = registry.GetAllEndpoints(); + + // Assert + endpoints.Should().HaveCount(3); + endpoints.Should().Contain(endpoint1); + endpoints.Should().Contain(endpoint2); + endpoints.Should().Contain(endpoint3); + } + + [Fact] + public void GetAllEndpoints_PreservesRegistrationOrder() + { + // Arrange + var registry = new EndpointRegistry(); + var endpoint1 = CreateEndpoint("GET", "/first"); + var endpoint2 = CreateEndpoint("GET", "/second"); + var endpoint3 = CreateEndpoint("GET", "/third"); + registry.Register(endpoint1); + registry.Register(endpoint2); + registry.Register(endpoint3); + + // Act + var endpoints = registry.GetAllEndpoints(); + + // Assert + endpoints[0].Should().Be(endpoint1); + endpoints[1].Should().Be(endpoint2); + endpoints[2].Should().Be(endpoint3); + } + + #endregion + + #region Constructor Tests + + [Fact] + public void Constructor_DefaultCaseInsensitive_IsTrue() + { + // Arrange + var registry = new EndpointRegistry(); + registry.Register(CreateEndpoint("GET", "/api/Test")); + + // Act & Assert - should match case-insensitively by default + registry.TryMatch("GET", "/api/test", out _).Should().BeTrue(); + } + + [Fact] + public void Constructor_ExplicitCaseInsensitiveFalse_IsCaseSensitive() + { + // Arrange + var registry = new EndpointRegistry(caseInsensitive: false); + registry.Register(CreateEndpoint("GET", "/api/Test")); + + // Act & Assert + registry.TryMatch("GET", "/api/Test", out _).Should().BeTrue(); + registry.TryMatch("GET", "/api/test", out _).Should().BeFalse(); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Microservice.Tests/HeaderCollectionTests.cs b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/HeaderCollectionTests.cs new file mode 100644 index 000000000..6afba980e --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/HeaderCollectionTests.cs @@ -0,0 +1,383 @@ +namespace StellaOps.Microservice.Tests; + +/// +/// Unit tests for . +/// +public sealed class HeaderCollectionTests +{ + #region Constructor Tests + + [Fact] + public void Constructor_Default_CreatesEmptyCollection() + { + // Arrange & Act + var headers = new HeaderCollection(); + + // Assert + headers.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WithKeyValuePairs_AddsAllHeaders() + { + // Arrange + var pairs = new[] + { + new KeyValuePair("Content-Type", "application/json"), + new KeyValuePair("Accept", "application/json") + }; + + // Act + var headers = new HeaderCollection(pairs); + + // Assert + headers["Content-Type"].Should().Be("application/json"); + headers["Accept"].Should().Be("application/json"); + } + + [Fact] + public void Constructor_WithDuplicateKeys_AddsMultipleValues() + { + // Arrange + var pairs = new[] + { + new KeyValuePair("Accept", "application/json"), + new KeyValuePair("Accept", "text/plain") + }; + + // Act + var headers = new HeaderCollection(pairs); + + // Assert + headers.GetValues("Accept").Should().BeEquivalentTo(["application/json", "text/plain"]); + } + + #endregion + + #region Empty Tests + + [Fact] + public void Empty_IsSharedInstance() + { + // Arrange & Act + var empty1 = HeaderCollection.Empty; + var empty2 = HeaderCollection.Empty; + + // Assert + empty1.Should().BeSameAs(empty2); + } + + [Fact] + public void Empty_HasNoHeaders() + { + // Arrange & Act + var empty = HeaderCollection.Empty; + + // Assert + empty.Should().BeEmpty(); + } + + #endregion + + #region Indexer Tests + + [Fact] + public void Indexer_ExistingKey_ReturnsFirstValue() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act + var value = headers["Content-Type"]; + + // Assert + value.Should().Be("application/json"); + } + + [Fact] + public void Indexer_MultipleValues_ReturnsFirstValue() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Accept", "application/json"); + headers.Add("Accept", "text/plain"); + + // Act + var value = headers["Accept"]; + + // Assert + value.Should().Be("application/json"); + } + + [Fact] + public void Indexer_NonexistentKey_ReturnsNull() + { + // Arrange + var headers = new HeaderCollection(); + + // Act + var value = headers["X-Missing"]; + + // Assert + value.Should().BeNull(); + } + + [Fact] + public void Indexer_IsCaseInsensitive() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act & Assert + headers["content-type"].Should().Be("application/json"); + headers["CONTENT-TYPE"].Should().Be("application/json"); + headers["Content-TYPE"].Should().Be("application/json"); + } + + #endregion + + #region Add Tests + + [Fact] + public void Add_NewKey_AddsHeader() + { + // Arrange + var headers = new HeaderCollection(); + + // Act + headers.Add("Content-Type", "application/json"); + + // Assert + headers["Content-Type"].Should().Be("application/json"); + } + + [Fact] + public void Add_ExistingKey_AppendsValue() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Accept", "application/json"); + + // Act + headers.Add("Accept", "text/plain"); + + // Assert + headers.GetValues("Accept").Should().HaveCount(2); + } + + [Fact] + public void Add_CaseInsensitiveKey_AppendsToExisting() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act + headers.Add("content-type", "text/plain"); + + // Assert + headers.GetValues("Content-Type").Should().HaveCount(2); + } + + #endregion + + #region Set Tests + + [Fact] + public void Set_NewKey_AddsHeader() + { + // Arrange + var headers = new HeaderCollection(); + + // Act + headers.Set("Content-Type", "application/json"); + + // Assert + headers["Content-Type"].Should().Be("application/json"); + } + + [Fact] + public void Set_ExistingKey_ReplacesValue() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "text/plain"); + headers.Add("Content-Type", "text/html"); + + // Act + headers.Set("Content-Type", "application/json"); + + // Assert + headers.GetValues("Content-Type").Should().BeEquivalentTo(["application/json"]); + } + + #endregion + + #region GetValues Tests + + [Fact] + public void GetValues_ExistingKey_ReturnsAllValues() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Accept", "application/json"); + headers.Add("Accept", "text/plain"); + headers.Add("Accept", "text/html"); + + // Act + var values = headers.GetValues("Accept"); + + // Assert + values.Should().BeEquivalentTo(["application/json", "text/plain", "text/html"]); + } + + [Fact] + public void GetValues_NonexistentKey_ReturnsEmptyEnumerable() + { + // Arrange + var headers = new HeaderCollection(); + + // Act + var values = headers.GetValues("X-Missing"); + + // Assert + values.Should().BeEmpty(); + } + + [Fact] + public void GetValues_IsCaseInsensitive() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Accept", "application/json"); + + // Act & Assert + headers.GetValues("accept").Should().Contain("application/json"); + headers.GetValues("ACCEPT").Should().Contain("application/json"); + } + + #endregion + + #region TryGetValue Tests + + [Fact] + public void TryGetValue_ExistingKey_ReturnsTrueAndValue() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act + var result = headers.TryGetValue("Content-Type", out var value); + + // Assert + result.Should().BeTrue(); + value.Should().Be("application/json"); + } + + [Fact] + public void TryGetValue_NonexistentKey_ReturnsFalse() + { + // Arrange + var headers = new HeaderCollection(); + + // Act + var result = headers.TryGetValue("X-Missing", out var value); + + // Assert + result.Should().BeFalse(); + value.Should().BeNull(); + } + + [Fact] + public void TryGetValue_IsCaseInsensitive() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act + var result = headers.TryGetValue("content-type", out var value); + + // Assert + result.Should().BeTrue(); + value.Should().Be("application/json"); + } + + #endregion + + #region ContainsKey Tests + + [Fact] + public void ContainsKey_ExistingKey_ReturnsTrue() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act & Assert + headers.ContainsKey("Content-Type").Should().BeTrue(); + } + + [Fact] + public void ContainsKey_NonexistentKey_ReturnsFalse() + { + // Arrange + var headers = new HeaderCollection(); + + // Act & Assert + headers.ContainsKey("X-Missing").Should().BeFalse(); + } + + [Fact] + public void ContainsKey_IsCaseInsensitive() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + // Act & Assert + headers.ContainsKey("content-type").Should().BeTrue(); + headers.ContainsKey("CONTENT-TYPE").Should().BeTrue(); + } + + #endregion + + #region Enumeration Tests + + [Fact] + public void GetEnumerator_EnumeratesAllHeaderValues() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + headers.Add("Accept", "text/plain"); + headers.Add("Accept", "text/html"); + + // Act + var list = headers.ToList(); + + // Assert + list.Should().HaveCount(3); + list.Should().Contain(new KeyValuePair("Content-Type", "application/json")); + list.Should().Contain(new KeyValuePair("Accept", "text/plain")); + list.Should().Contain(new KeyValuePair("Accept", "text/html")); + } + + [Fact] + public void GetEnumerator_EmptyCollection_EnumeratesNothing() + { + // Arrange + var headers = new HeaderCollection(); + + // Act + var list = headers.ToList(); + + // Assert + list.Should().BeEmpty(); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Microservice.Tests/InflightRequestTrackerTests.cs b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/InflightRequestTrackerTests.cs new file mode 100644 index 000000000..626b778ae --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/InflightRequestTrackerTests.cs @@ -0,0 +1,309 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace StellaOps.Microservice.Tests; + +/// +/// Unit tests for . +/// +public sealed class InflightRequestTrackerTests : IDisposable +{ + private readonly InflightRequestTracker _tracker; + + public InflightRequestTrackerTests() + { + _tracker = new InflightRequestTracker(NullLogger.Instance); + } + + public void Dispose() + { + _tracker.Dispose(); + } + + #region Track Tests + + [Fact] + public void Track_NewRequest_ReturnsNonCancelledToken() + { + // Arrange + var correlationId = Guid.NewGuid(); + + // Act + var token = _tracker.Track(correlationId); + + // Assert + token.IsCancellationRequested.Should().BeFalse(); + } + + [Fact] + public void Track_NewRequest_IncreasesCount() + { + // Arrange + var correlationId = Guid.NewGuid(); + + // Act + _tracker.Track(correlationId); + + // Assert + _tracker.Count.Should().Be(1); + } + + [Fact] + public void Track_MultipleRequests_TracksAll() + { + // Arrange & Act + _tracker.Track(Guid.NewGuid()); + _tracker.Track(Guid.NewGuid()); + _tracker.Track(Guid.NewGuid()); + + // Assert + _tracker.Count.Should().Be(3); + } + + [Fact] + public void Track_DuplicateCorrelationId_ThrowsInvalidOperationException() + { + // Arrange + var correlationId = Guid.NewGuid(); + _tracker.Track(correlationId); + + // Act + var action = () => _tracker.Track(correlationId); + + // Assert + action.Should().Throw() + .WithMessage($"*{correlationId}*already being tracked*"); + } + + [Fact] + public void Track_AfterDispose_ThrowsObjectDisposedException() + { + // Arrange + _tracker.Dispose(); + + // Act + var action = () => _tracker.Track(Guid.NewGuid()); + + // Assert + action.Should().Throw(); + } + + #endregion + + #region Cancel Tests + + [Fact] + public void Cancel_TrackedRequest_CancelsToken() + { + // Arrange + var correlationId = Guid.NewGuid(); + var token = _tracker.Track(correlationId); + + // Act + var result = _tracker.Cancel(correlationId, "Test cancellation"); + + // Assert + result.Should().BeTrue(); + token.IsCancellationRequested.Should().BeTrue(); + } + + [Fact] + public void Cancel_UntrackedRequest_ReturnsFalse() + { + // Arrange + var correlationId = Guid.NewGuid(); + + // Act + var result = _tracker.Cancel(correlationId, "Test cancellation"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void Cancel_WithNullReason_Works() + { + // Arrange + var correlationId = Guid.NewGuid(); + _tracker.Track(correlationId); + + // Act + var result = _tracker.Cancel(correlationId, null); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void Cancel_CompletedRequest_ReturnsFalse() + { + // Arrange + var correlationId = Guid.NewGuid(); + _tracker.Track(correlationId); + _tracker.Complete(correlationId); + + // Act + var result = _tracker.Cancel(correlationId, "Test cancellation"); + + // Assert + result.Should().BeFalse(); + } + + #endregion + + #region Complete Tests + + [Fact] + public void Complete_TrackedRequest_RemovesFromTracking() + { + // Arrange + var correlationId = Guid.NewGuid(); + _tracker.Track(correlationId); + + // Act + _tracker.Complete(correlationId); + + // Assert + _tracker.Count.Should().Be(0); + } + + [Fact] + public void Complete_UntrackedRequest_DoesNotThrow() + { + // Arrange + var correlationId = Guid.NewGuid(); + + // Act + var action = () => _tracker.Complete(correlationId); + + // Assert + action.Should().NotThrow(); + } + + [Fact] + public void Complete_MultipleCompletions_DoesNotThrow() + { + // Arrange + var correlationId = Guid.NewGuid(); + _tracker.Track(correlationId); + + // Act + var action = () => + { + _tracker.Complete(correlationId); + _tracker.Complete(correlationId); + }; + + // Assert + action.Should().NotThrow(); + } + + #endregion + + #region CancelAll Tests + + [Fact] + public void CancelAll_CancelsAllTrackedRequests() + { + // Arrange + var token1 = _tracker.Track(Guid.NewGuid()); + var token2 = _tracker.Track(Guid.NewGuid()); + var token3 = _tracker.Track(Guid.NewGuid()); + + // Act + _tracker.CancelAll("Shutdown"); + + // Assert + token1.IsCancellationRequested.Should().BeTrue(); + token2.IsCancellationRequested.Should().BeTrue(); + token3.IsCancellationRequested.Should().BeTrue(); + } + + [Fact] + public void CancelAll_ClearsTrackedRequests() + { + // Arrange + _tracker.Track(Guid.NewGuid()); + _tracker.Track(Guid.NewGuid()); + + // Act + _tracker.CancelAll("Shutdown"); + + // Assert + _tracker.Count.Should().Be(0); + } + + [Fact] + public void CancelAll_WithNoRequests_DoesNotThrow() + { + // Arrange & Act + var action = () => _tracker.CancelAll("Test"); + + // Assert + action.Should().NotThrow(); + } + + #endregion + + #region Dispose Tests + + [Fact] + public void Dispose_CancelsAllRequests() + { + // Arrange + var token = _tracker.Track(Guid.NewGuid()); + + // Act + _tracker.Dispose(); + + // Assert + token.IsCancellationRequested.Should().BeTrue(); + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + // Arrange & Act + var action = () => + { + _tracker.Dispose(); + _tracker.Dispose(); + _tracker.Dispose(); + }; + + // Assert + action.Should().NotThrow(); + } + + #endregion + + #region Count Tests + + [Fact] + public void Count_InitiallyZero() + { + // Arrange - use a fresh tracker + using var tracker = new InflightRequestTracker(NullLogger.Instance); + + // Assert + tracker.Count.Should().Be(0); + } + + [Fact] + public void Count_ReflectsActiveRequests() + { + // Arrange + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + // Act + _tracker.Track(id1); + _tracker.Track(id2); + _tracker.Complete(id1); + + // Assert + _tracker.Count.Should().Be(1); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Microservice.Tests/RawRequestContextTests.cs b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/RawRequestContextTests.cs new file mode 100644 index 000000000..90c4c424a --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/RawRequestContextTests.cs @@ -0,0 +1,272 @@ +namespace StellaOps.Microservice.Tests; + +/// +/// Unit tests for . +/// +public sealed class RawRequestContextTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_Method_DefaultsToEmptyString() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.Method.Should().BeEmpty(); + } + + [Fact] + public void Constructor_Path_DefaultsToEmptyString() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.Path.Should().BeEmpty(); + } + + [Fact] + public void Constructor_PathParameters_DefaultsToEmptyDictionary() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.PathParameters.Should().NotBeNull(); + context.PathParameters.Should().BeEmpty(); + } + + [Fact] + public void Constructor_Headers_DefaultsToEmptyCollection() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.Headers.Should().BeSameAs(HeaderCollection.Empty); + } + + [Fact] + public void Constructor_Body_DefaultsToStreamNull() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.Body.Should().BeSameAs(Stream.Null); + } + + [Fact] + public void Constructor_CancellationToken_DefaultsToNone() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.CancellationToken.Should().Be(CancellationToken.None); + } + + [Fact] + public void Constructor_CorrelationId_DefaultsToNull() + { + // Arrange & Act + var context = new RawRequestContext(); + + // Assert + context.CorrelationId.Should().BeNull(); + } + + #endregion + + #region Property Initialization Tests + + [Fact] + public void Method_CanBeInitialized() + { + // Arrange & Act + var context = new RawRequestContext { Method = "POST" }; + + // Assert + context.Method.Should().Be("POST"); + } + + [Fact] + public void Path_CanBeInitialized() + { + // Arrange & Act + var context = new RawRequestContext { Path = "/api/users/123" }; + + // Assert + context.Path.Should().Be("/api/users/123"); + } + + [Fact] + public void PathParameters_CanBeInitialized() + { + // Arrange + var parameters = new Dictionary + { + ["id"] = "123", + ["action"] = "update" + }; + + // Act + var context = new RawRequestContext { PathParameters = parameters }; + + // Assert + context.PathParameters.Should().HaveCount(2); + context.PathParameters["id"].Should().Be("123"); + context.PathParameters["action"].Should().Be("update"); + } + + [Fact] + public void Headers_CanBeInitialized() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + headers.Add("Authorization", "Bearer token"); + + // Act + var context = new RawRequestContext { Headers = headers }; + + // Assert + context.Headers["Content-Type"].Should().Be("application/json"); + context.Headers["Authorization"].Should().Be("Bearer token"); + } + + [Fact] + public void Body_CanBeInitialized() + { + // Arrange + var body = new MemoryStream([1, 2, 3, 4, 5]); + + // Act + var context = new RawRequestContext { Body = body }; + + // Assert + context.Body.Should().BeSameAs(body); + } + + [Fact] + public void CancellationToken_CanBeInitialized() + { + // Arrange + using var cts = new CancellationTokenSource(); + + // Act + var context = new RawRequestContext { CancellationToken = cts.Token }; + + // Assert + context.CancellationToken.Should().Be(cts.Token); + } + + [Fact] + public void CorrelationId_CanBeInitialized() + { + // Arrange & Act + var context = new RawRequestContext { CorrelationId = "req-12345" }; + + // Assert + context.CorrelationId.Should().Be("req-12345"); + } + + #endregion + + #region Complete Context Tests + + [Fact] + public void CompleteContext_AllPropertiesSet_Works() + { + // Arrange + var headers = new HeaderCollection(); + headers.Add("Content-Type", "application/json"); + + var body = new MemoryStream([123, 125]); // "{}" + + using var cts = new CancellationTokenSource(); + + // Act + var context = new RawRequestContext + { + Method = "POST", + Path = "/api/users/{id}", + PathParameters = new Dictionary { ["id"] = "456" }, + Headers = headers, + Body = body, + CancellationToken = cts.Token, + CorrelationId = "corr-789" + }; + + // Assert + context.Method.Should().Be("POST"); + context.Path.Should().Be("/api/users/{id}"); + context.PathParameters["id"].Should().Be("456"); + context.Headers["Content-Type"].Should().Be("application/json"); + context.Body.Should().BeSameAs(body); + context.CancellationToken.Should().Be(cts.Token); + context.CorrelationId.Should().Be("corr-789"); + } + + [Fact] + public void Context_WithCancelledToken_HasCancellationRequested() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act + var context = new RawRequestContext { CancellationToken = cts.Token }; + + // Assert + context.CancellationToken.IsCancellationRequested.Should().BeTrue(); + } + + #endregion + + #region Typical Use Case Tests + + [Fact] + public void TypicalGetRequest_HasMinimalProperties() + { + // Arrange & Act + var context = new RawRequestContext + { + Method = "GET", + Path = "/api/health" + }; + + // Assert + context.Method.Should().Be("GET"); + context.Path.Should().Be("/api/health"); + context.Body.Should().BeSameAs(Stream.Null); + context.Headers.Should().BeEmpty(); + } + + [Fact] + public void TypicalPostRequest_HasBodyAndHeaders() + { + // Arrange + var headers = new HeaderCollection(); + headers.Set("Content-Type", "application/json"); + var body = new MemoryStream([123, 34, 110, 97, 109, 101, 34, 58, 34, 116, 101, 115, 116, 34, 125]); // {"name":"test"} + + // Act + var context = new RawRequestContext + { + Method = "POST", + Path = "/api/users", + Headers = headers, + Body = body + }; + + // Assert + context.Method.Should().Be("POST"); + context.Headers["Content-Type"].Should().Be("application/json"); + context.Body.Length.Should().BeGreaterThan(0); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Microservice.Tests/RawResponseTests.cs b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/RawResponseTests.cs new file mode 100644 index 000000000..808da9382 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/RawResponseTests.cs @@ -0,0 +1,354 @@ +using System.Text; + +namespace StellaOps.Microservice.Tests; + +/// +/// Unit tests for . +/// +public sealed class RawResponseTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_StatusCode_DefaultsTo200() + { + // Arrange & Act + var response = new RawResponse(); + + // Assert + response.StatusCode.Should().Be(200); + } + + [Fact] + public void Constructor_Headers_DefaultsToEmpty() + { + // Arrange & Act + var response = new RawResponse(); + + // Assert + response.Headers.Should().BeSameAs(HeaderCollection.Empty); + } + + [Fact] + public void Constructor_Body_DefaultsToStreamNull() + { + // Arrange & Act + var response = new RawResponse(); + + // Assert + response.Body.Should().BeSameAs(Stream.Null); + } + + #endregion + + #region Ok Factory Method Tests + + [Fact] + public void Ok_WithStream_CreatesOkResponse() + { + // Arrange + var stream = new MemoryStream([1, 2, 3, 4, 5]); + + // Act + var response = RawResponse.Ok(stream); + + // Assert + response.StatusCode.Should().Be(200); + response.Body.Should().BeSameAs(stream); + } + + [Fact] + public void Ok_WithByteArray_CreatesOkResponse() + { + // Arrange + var data = new byte[] { 1, 2, 3, 4, 5 }; + + // Act + var response = RawResponse.Ok(data); + + // Assert + response.StatusCode.Should().Be(200); + response.Body.Should().BeOfType(); + ((MemoryStream)response.Body).ToArray().Should().BeEquivalentTo(data); + } + + [Fact] + public void Ok_WithString_CreatesOkResponse() + { + // Arrange + var text = "Hello, World!"; + + // Act + var response = RawResponse.Ok(text); + + // Assert + response.StatusCode.Should().Be(200); + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be(text); + } + + [Fact] + public void Ok_WithEmptyString_CreatesOkResponse() + { + // Arrange & Act + var response = RawResponse.Ok(""); + + // Assert + response.StatusCode.Should().Be(200); + response.Body.Length.Should().Be(0); + } + + #endregion + + #region NoContent Factory Method Tests + + [Fact] + public void NoContent_Creates204Response() + { + // Arrange & Act + var response = RawResponse.NoContent(); + + // Assert + response.StatusCode.Should().Be(204); + } + + [Fact] + public void NoContent_HasDefaultHeaders() + { + // Arrange & Act + var response = RawResponse.NoContent(); + + // Assert + response.Headers.Should().BeSameAs(HeaderCollection.Empty); + } + + [Fact] + public void NoContent_HasDefaultBody() + { + // Arrange & Act + var response = RawResponse.NoContent(); + + // Assert + response.Body.Should().BeSameAs(Stream.Null); + } + + #endregion + + #region BadRequest Factory Method Tests + + [Fact] + public void BadRequest_Creates400Response() + { + // Arrange & Act + var response = RawResponse.BadRequest(); + + // Assert + response.StatusCode.Should().Be(400); + } + + [Fact] + public void BadRequest_WithDefaultMessage_HasBadRequestText() + { + // Arrange & Act + var response = RawResponse.BadRequest(); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be("Bad Request"); + } + + [Fact] + public void BadRequest_WithCustomMessage_HasCustomText() + { + // Arrange & Act + var response = RawResponse.BadRequest("Invalid input"); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be("Invalid input"); + } + + [Fact] + public void BadRequest_SetsTextPlainContentType() + { + // Arrange & Act + var response = RawResponse.BadRequest(); + + // Assert + response.Headers["Content-Type"].Should().Be("text/plain; charset=utf-8"); + } + + #endregion + + #region NotFound Factory Method Tests + + [Fact] + public void NotFound_Creates404Response() + { + // Arrange & Act + var response = RawResponse.NotFound(); + + // Assert + response.StatusCode.Should().Be(404); + } + + [Fact] + public void NotFound_WithDefaultMessage_HasNotFoundText() + { + // Arrange & Act + var response = RawResponse.NotFound(); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be("Not Found"); + } + + [Fact] + public void NotFound_WithCustomMessage_HasCustomText() + { + // Arrange & Act + var response = RawResponse.NotFound("Resource does not exist"); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be("Resource does not exist"); + } + + #endregion + + #region InternalError Factory Method Tests + + [Fact] + public void InternalError_Creates500Response() + { + // Arrange & Act + var response = RawResponse.InternalError(); + + // Assert + response.StatusCode.Should().Be(500); + } + + [Fact] + public void InternalError_WithDefaultMessage_HasInternalServerErrorText() + { + // Arrange & Act + var response = RawResponse.InternalError(); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be("Internal Server Error"); + } + + [Fact] + public void InternalError_WithCustomMessage_HasCustomText() + { + // Arrange & Act + var response = RawResponse.InternalError("Database connection failed"); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be("Database connection failed"); + } + + #endregion + + #region Error Factory Method Tests + + [Theory] + [InlineData(400, "Bad Request")] + [InlineData(401, "Unauthorized")] + [InlineData(403, "Forbidden")] + [InlineData(404, "Not Found")] + [InlineData(500, "Internal Server Error")] + [InlineData(502, "Bad Gateway")] + [InlineData(503, "Service Unavailable")] + public void Error_CreatesResponseWithCorrectStatusCode(int statusCode, string message) + { + // Arrange & Act + var response = RawResponse.Error(statusCode, message); + + // Assert + response.StatusCode.Should().Be(statusCode); + } + + [Fact] + public void Error_SetsCorrectContentType() + { + // Arrange & Act + var response = RawResponse.Error(418, "I'm a teapot"); + + // Assert + response.Headers["Content-Type"].Should().Be("text/plain; charset=utf-8"); + } + + [Fact] + public void Error_SetsMessageInBody() + { + // Arrange + var message = "Custom error message"; + + // Act + var response = RawResponse.Error(400, message); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be(message); + } + + [Fact] + public void Error_WithUnicodeMessage_EncodesCorrectly() + { + // Arrange + var message = "Error: \u4e2d\u6587\u6d88\u606f"; + + // Act + var response = RawResponse.Error(400, message); + + // Assert + using var reader = new StreamReader(response.Body, Encoding.UTF8); + reader.ReadToEnd().Should().Be(message); + } + + #endregion + + #region Property Initialization Tests + + [Fact] + public void StatusCode_CanBeInitialized() + { + // Arrange & Act + var response = new RawResponse { StatusCode = 201 }; + + // Assert + response.StatusCode.Should().Be(201); + } + + [Fact] + public void Headers_CanBeInitialized() + { + // Arrange + var headers = new HeaderCollection(); + headers.Set("X-Custom", "value"); + + // Act + var response = new RawResponse { Headers = headers }; + + // Assert + response.Headers["X-Custom"].Should().Be("value"); + } + + [Fact] + public void Body_CanBeInitialized() + { + // Arrange + var stream = new MemoryStream([1, 2, 3]); + + // Act + var response = new RawResponse { Body = stream }; + + // Assert + response.Body.Should().BeSameAs(stream); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Microservice.Tests/StellaOps.Microservice.Tests.csproj b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/StellaOps.Microservice.Tests.csproj new file mode 100644 index 000000000..39c7afb8e --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Microservice.Tests/StellaOps.Microservice.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + preview + enable + enable + true + + $(NoWarn);CA2255 + false + StellaOps.Microservice.Tests + + false + + + + + + + + + + + + + + + + + + diff --git a/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/FrameConverterTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/FrameConverterTests.cs new file mode 100644 index 000000000..2f0512903 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/FrameConverterTests.cs @@ -0,0 +1,550 @@ +using System.Text; +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Frames; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Common.Tests; + +/// +/// Unit tests for . +/// +public sealed class FrameConverterTests +{ + #region ToFrame (RequestFrame) Tests + + [Fact] + public void ToFrame_RequestFrame_ReturnsFrameWithRequestType() + { + // Arrange + var request = CreateTestRequestFrame(); + + // Act + var frame = FrameConverter.ToFrame(request); + + // Assert + frame.Type.Should().Be(FrameType.Request); + } + + [Fact] + public void ToFrame_RequestFrame_SetsCorrelationIdFromRequest() + { + // Arrange + var request = CreateTestRequestFrame(correlationId: "test-correlation-123"); + + // Act + var frame = FrameConverter.ToFrame(request); + + // Assert + frame.CorrelationId.Should().Be("test-correlation-123"); + } + + [Fact] + public void ToFrame_RequestFrame_UsesRequestIdWhenCorrelationIdIsNull() + { + // Arrange + var request = new RequestFrame + { + RequestId = "request-id-456", + CorrelationId = null, + Method = "GET", + Path = "/test" + }; + + // Act + var frame = FrameConverter.ToFrame(request); + + // Assert + frame.CorrelationId.Should().Be("request-id-456"); + } + + [Fact] + public void ToFrame_RequestFrame_SerializesPayload() + { + // Arrange + var request = CreateTestRequestFrame(); + + // Act + var frame = FrameConverter.ToFrame(request); + + // Assert + frame.Payload.Length.Should().BeGreaterThan(0); + } + + #endregion + + #region ToRequestFrame Tests + + [Fact] + public void ToRequestFrame_ValidRequestFrame_ReturnsRequestFrame() + { + // Arrange + var originalRequest = CreateTestRequestFrame(); + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public void ToRequestFrame_WrongFrameType_ReturnsNull() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Response, + CorrelationId = "test", + Payload = Array.Empty() + }; + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void ToRequestFrame_InvalidJson_ReturnsNull() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Encoding.UTF8.GetBytes("invalid json {{{") + }; + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesRequestId() + { + // Arrange + var originalRequest = CreateTestRequestFrame(requestId: "unique-request-id"); + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.RequestId.Should().Be("unique-request-id"); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesMethod() + { + // Arrange + var originalRequest = CreateTestRequestFrame(method: "DELETE"); + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.Method.Should().Be("DELETE"); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesPath() + { + // Arrange + var originalRequest = CreateTestRequestFrame(path: "/api/users/123"); + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.Path.Should().Be("/api/users/123"); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesHeaders() + { + // Arrange + var headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["X-Custom-Header"] = "custom-value" + }; + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "POST", + Path = "/test", + Headers = headers + }; + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.Headers.Should().ContainKey("Content-Type"); + result.Headers["Content-Type"].Should().Be("application/json"); + result.Headers["X-Custom-Header"].Should().Be("custom-value"); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesPayload() + { + // Arrange + var payloadBytes = Encoding.UTF8.GetBytes("{\"key\":\"value\"}"); + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "POST", + Path = "/test", + Payload = payloadBytes + }; + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.Payload.ToArray().Should().BeEquivalentTo(payloadBytes); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesTimeoutSeconds() + { + // Arrange + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "GET", + Path = "/test", + TimeoutSeconds = 60 + }; + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.TimeoutSeconds.Should().Be(60); + } + + [Fact] + public void ToRequestFrame_RoundTrip_PreservesSupportsStreaming() + { + // Arrange + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "GET", + Path = "/test", + SupportsStreaming = true + }; + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.SupportsStreaming.Should().BeTrue(); + } + + #endregion + + #region ToFrame (ResponseFrame) Tests + + [Fact] + public void ToFrame_ResponseFrame_ReturnsFrameWithResponseType() + { + // Arrange + var response = CreateTestResponseFrame(); + + // Act + var frame = FrameConverter.ToFrame(response); + + // Assert + frame.Type.Should().Be(FrameType.Response); + } + + [Fact] + public void ToFrame_ResponseFrame_SetsCorrelationIdToRequestId() + { + // Arrange + var response = CreateTestResponseFrame(requestId: "req-123"); + + // Act + var frame = FrameConverter.ToFrame(response); + + // Assert + frame.CorrelationId.Should().Be("req-123"); + } + + #endregion + + #region ToResponseFrame Tests + + [Fact] + public void ToResponseFrame_ValidResponseFrame_ReturnsResponseFrame() + { + // Arrange + var originalResponse = CreateTestResponseFrame(); + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public void ToResponseFrame_WrongFrameType_ReturnsNull() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void ToResponseFrame_InvalidJson_ReturnsNull() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Response, + CorrelationId = "test", + Payload = Encoding.UTF8.GetBytes("not valid json") + }; + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public void ToResponseFrame_RoundTrip_PreservesRequestId() + { + // Arrange + var originalResponse = CreateTestResponseFrame(requestId: "original-req-id"); + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result!.RequestId.Should().Be("original-req-id"); + } + + [Fact] + public void ToResponseFrame_RoundTrip_PreservesStatusCode() + { + // Arrange + var originalResponse = CreateTestResponseFrame(statusCode: 404); + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result!.StatusCode.Should().Be(404); + } + + [Fact] + public void ToResponseFrame_RoundTrip_PreservesHeaders() + { + // Arrange + var headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["Cache-Control"] = "no-cache" + }; + var originalResponse = new ResponseFrame + { + RequestId = "test-id", + StatusCode = 200, + Headers = headers + }; + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result!.Headers["Content-Type"].Should().Be("application/json"); + result.Headers["Cache-Control"].Should().Be("no-cache"); + } + + [Fact] + public void ToResponseFrame_RoundTrip_PreservesPayload() + { + // Arrange + var payloadBytes = Encoding.UTF8.GetBytes("{\"result\":\"success\"}"); + var originalResponse = new ResponseFrame + { + RequestId = "test-id", + StatusCode = 200, + Payload = payloadBytes + }; + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result!.Payload.ToArray().Should().BeEquivalentTo(payloadBytes); + } + + [Fact] + public void ToResponseFrame_RoundTrip_PreservesHasMoreChunks() + { + // Arrange + var originalResponse = new ResponseFrame + { + RequestId = "test-id", + StatusCode = 200, + HasMoreChunks = true + }; + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result!.HasMoreChunks.Should().BeTrue(); + } + + #endregion + + #region Edge Cases + + [Fact] + public void ToRequestFrame_EmptyPayload_ReturnsEmptyPayload() + { + // Arrange + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "GET", + Path = "/test", + Payload = Array.Empty() + }; + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.Payload.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void ToRequestFrame_NullHeaders_ReturnsEmptyHeaders() + { + // Arrange + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "GET", + Path = "/test" + }; + var frame = FrameConverter.ToFrame(originalRequest); + + // Act + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result!.Headers.Should().NotBeNull(); + result.Headers.Should().BeEmpty(); + } + + [Fact] + public void ToResponseFrame_EmptyPayload_ReturnsEmptyPayload() + { + // Arrange + var originalResponse = new ResponseFrame + { + RequestId = "test-id", + StatusCode = 204, + Payload = Array.Empty() + }; + var frame = FrameConverter.ToFrame(originalResponse); + + // Act + var result = FrameConverter.ToResponseFrame(frame); + + // Assert + result!.Payload.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void ToFrame_LargePayload_Succeeds() + { + // Arrange + var largePayload = new byte[1024 * 1024]; // 1MB + Random.Shared.NextBytes(largePayload); + var originalRequest = new RequestFrame + { + RequestId = "test-id", + Method = "POST", + Path = "/upload", + Payload = largePayload + }; + + // Act + var frame = FrameConverter.ToFrame(originalRequest); + var result = FrameConverter.ToRequestFrame(frame); + + // Assert + result.Should().NotBeNull(); + result!.Payload.ToArray().Should().BeEquivalentTo(largePayload); + } + + #endregion + + #region Helper Methods + + private static RequestFrame CreateTestRequestFrame( + string? requestId = null, + string? correlationId = null, + string method = "GET", + string path = "/test") + { + return new RequestFrame + { + RequestId = requestId ?? Guid.NewGuid().ToString("N"), + CorrelationId = correlationId, + Method = method, + Path = path + }; + } + + private static ResponseFrame CreateTestResponseFrame( + string? requestId = null, + int statusCode = 200) + { + return new ResponseFrame + { + RequestId = requestId ?? Guid.NewGuid().ToString("N"), + StatusCode = statusCode + }; + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/PathMatcherTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/PathMatcherTests.cs new file mode 100644 index 000000000..f254aae97 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/PathMatcherTests.cs @@ -0,0 +1,463 @@ +namespace StellaOps.Router.Common.Tests; + +/// +/// Unit tests for . +/// +public sealed class PathMatcherTests +{ + #region Constructor Tests + + [Fact] + public void Constructor_SetsTemplate() + { + // Arrange & Act + var matcher = new PathMatcher("/api/users/{id}"); + + // Assert + matcher.Template.Should().Be("/api/users/{id}"); + } + + [Fact] + public void Constructor_DefaultsCaseInsensitive() + { + // Arrange & Act + var matcher = new PathMatcher("/api/Users"); + + // Assert + matcher.IsMatch("/api/users").Should().BeTrue(); + } + + [Fact] + public void Constructor_CaseSensitive_DoesNotMatchDifferentCase() + { + // Arrange & Act + var matcher = new PathMatcher("/api/Users", caseInsensitive: false); + + // Assert + matcher.IsMatch("/api/users").Should().BeFalse(); + matcher.IsMatch("/api/Users").Should().BeTrue(); + } + + #endregion + + #region IsMatch Tests - Exact Paths + + [Fact] + public void IsMatch_ExactPath_ReturnsTrue() + { + // Arrange + var matcher = new PathMatcher("/api/health"); + + // Act & Assert + matcher.IsMatch("/api/health").Should().BeTrue(); + } + + [Fact] + public void IsMatch_ExactPath_TrailingSlash_ReturnsTrue() + { + // Arrange + var matcher = new PathMatcher("/api/health"); + + // Act & Assert + matcher.IsMatch("/api/health/").Should().BeTrue(); + } + + [Fact] + public void IsMatch_ExactPath_NoLeadingSlash_ReturnsTrue() + { + // Arrange + var matcher = new PathMatcher("/api/health"); + + // Act & Assert + matcher.IsMatch("api/health").Should().BeTrue(); + } + + [Fact] + public void IsMatch_DifferentPath_ReturnsFalse() + { + // Arrange + var matcher = new PathMatcher("/api/health"); + + // Act & Assert + matcher.IsMatch("/api/status").Should().BeFalse(); + } + + [Fact] + public void IsMatch_PartialPath_ReturnsFalse() + { + // Arrange + var matcher = new PathMatcher("/api/users/list"); + + // Act & Assert + matcher.IsMatch("/api/users").Should().BeFalse(); + } + + [Fact] + public void IsMatch_LongerPath_ReturnsFalse() + { + // Arrange + var matcher = new PathMatcher("/api/users"); + + // Act & Assert + matcher.IsMatch("/api/users/list").Should().BeFalse(); + } + + #endregion + + #region IsMatch Tests - Case Sensitivity + + [Fact] + public void IsMatch_CaseInsensitive_MatchesMixedCase() + { + // Arrange + var matcher = new PathMatcher("/api/users", caseInsensitive: true); + + // Act & Assert + matcher.IsMatch("/API/USERS").Should().BeTrue(); + matcher.IsMatch("/Api/Users").Should().BeTrue(); + matcher.IsMatch("/aPi/uSeRs").Should().BeTrue(); + } + + [Fact] + public void IsMatch_CaseSensitive_OnlyMatchesExactCase() + { + // Arrange + var matcher = new PathMatcher("/Api/Users", caseInsensitive: false); + + // Act & Assert + matcher.IsMatch("/Api/Users").Should().BeTrue(); + matcher.IsMatch("/api/users").Should().BeFalse(); + matcher.IsMatch("/API/USERS").Should().BeFalse(); + } + + #endregion + + #region TryMatch Tests - Single Parameter + + [Fact] + public void TryMatch_SingleParameter_ReturnsTrue() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}"); + + // Act + var result = matcher.TryMatch("/api/users/123", out var parameters); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void TryMatch_SingleParameter_ExtractsParameter() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}"); + + // Act + matcher.TryMatch("/api/users/123", out var parameters); + + // Assert + parameters.Should().ContainKey("id"); + parameters["id"].Should().Be("123"); + } + + [Fact] + public void TryMatch_SingleParameter_ExtractsGuidParameter() + { + // Arrange + var matcher = new PathMatcher("/api/users/{userId}"); + var guid = Guid.NewGuid().ToString(); + + // Act + matcher.TryMatch($"/api/users/{guid}", out var parameters); + + // Assert + parameters["userId"].Should().Be(guid); + } + + [Fact] + public void TryMatch_SingleParameter_ExtractsStringParameter() + { + // Arrange + var matcher = new PathMatcher("/api/users/{username}"); + + // Act + matcher.TryMatch("/api/users/john-doe", out var parameters); + + // Assert + parameters["username"].Should().Be("john-doe"); + } + + #endregion + + #region TryMatch Tests - Multiple Parameters + + [Fact] + public void TryMatch_MultipleParameters_ReturnsTrue() + { + // Arrange + var matcher = new PathMatcher("/api/users/{userId}/posts/{postId}"); + + // Act + var result = matcher.TryMatch("/api/users/123/posts/456", out _); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void TryMatch_MultipleParameters_ExtractsAllParameters() + { + // Arrange + var matcher = new PathMatcher("/api/users/{userId}/posts/{postId}"); + + // Act + matcher.TryMatch("/api/users/user-1/posts/post-2", out var parameters); + + // Assert + parameters.Should().ContainKey("userId"); + parameters.Should().ContainKey("postId"); + parameters["userId"].Should().Be("user-1"); + parameters["postId"].Should().Be("post-2"); + } + + [Fact] + public void TryMatch_ThreeParameters_ExtractsAllParameters() + { + // Arrange + var matcher = new PathMatcher("/api/org/{orgId}/users/{userId}/roles/{roleId}"); + + // Act + matcher.TryMatch("/api/org/acme/users/john/roles/admin", out var parameters); + + // Assert + parameters.Should().HaveCount(3); + parameters["orgId"].Should().Be("acme"); + parameters["userId"].Should().Be("john"); + parameters["roleId"].Should().Be("admin"); + } + + #endregion + + #region TryMatch Tests - Non-Matching + + [Fact] + public void TryMatch_NonMatchingPath_ReturnsFalse() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}"); + + // Act + var result = matcher.TryMatch("/api/posts/123", out var parameters); + + // Assert + result.Should().BeFalse(); + parameters.Should().BeEmpty(); + } + + [Fact] + public void TryMatch_MissingParameter_ReturnsFalse() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}/posts/{postId}"); + + // Act + var result = matcher.TryMatch("/api/users/123/posts", out var parameters); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void TryMatch_ExtraSegment_ReturnsFalse() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}"); + + // Act + var result = matcher.TryMatch("/api/users/123/extra", out _); + + // Assert + result.Should().BeFalse(); + } + + #endregion + + #region TryMatch Tests - Path Normalization + + [Fact] + public void TryMatch_TrailingSlash_Matches() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}"); + + // Act + var result = matcher.TryMatch("/api/users/123/", out var parameters); + + // Assert + result.Should().BeTrue(); + parameters["id"].Should().Be("123"); + } + + [Fact] + public void TryMatch_NoLeadingSlash_Matches() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id}"); + + // Act + var result = matcher.TryMatch("api/users/123", out var parameters); + + // Assert + result.Should().BeTrue(); + parameters["id"].Should().Be("123"); + } + + #endregion + + #region TryMatch Tests - Parameter Type Constraints + + [Fact] + public void TryMatch_ParameterWithTypeConstraint_ExtractsParameterName() + { + // Arrange + // The PathMatcher ignores type constraints but still extracts the parameter + var matcher = new PathMatcher("/api/users/{id:int}"); + + // Act + matcher.TryMatch("/api/users/123", out var parameters); + + // Assert + parameters.Should().ContainKey("id"); + parameters["id"].Should().Be("123"); + } + + [Fact] + public void TryMatch_ParameterWithGuidConstraint_ExtractsParameterName() + { + // Arrange + var matcher = new PathMatcher("/api/users/{id:guid}"); + + // Act + matcher.TryMatch("/api/users/abc-123", out var parameters); + + // Assert + parameters.Should().ContainKey("id"); + parameters["id"].Should().Be("abc-123"); + } + + #endregion + + #region Edge Cases + + [Fact] + public void TryMatch_RootPath_Matches() + { + // Arrange + var matcher = new PathMatcher("/"); + + // Act + var result = matcher.TryMatch("/", out var parameters); + + // Assert + result.Should().BeTrue(); + parameters.Should().BeEmpty(); + } + + [Fact] + public void TryMatch_SingleSegmentWithParameter_Matches() + { + // Arrange + var matcher = new PathMatcher("/{id}"); + + // Act + var result = matcher.TryMatch("/test-value", out var parameters); + + // Assert + result.Should().BeTrue(); + parameters["id"].Should().Be("test-value"); + } + + [Fact] + public void IsMatch_EmptyPath_HandlesGracefully() + { + // Arrange + var matcher = new PathMatcher("/"); + + // Act + var result = matcher.IsMatch(""); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void TryMatch_ParameterWithHyphen_Extracts() + { + // Arrange + var matcher = new PathMatcher("/api/users/{user-id}"); + + // Act + matcher.TryMatch("/api/users/123", out var parameters); + + // Assert + parameters.Should().ContainKey("user-id"); + parameters["user-id"].Should().Be("123"); + } + + [Fact] + public void TryMatch_ParameterWithUnderscore_Extracts() + { + // Arrange + var matcher = new PathMatcher("/api/users/{user_id}"); + + // Act + matcher.TryMatch("/api/users/456", out var parameters); + + // Assert + parameters.Should().ContainKey("user_id"); + } + + [Fact] + public void TryMatch_SpecialCharactersInPath_Matches() + { + // Arrange + var matcher = new PathMatcher("/api/search/{query}"); + + // Act + matcher.TryMatch("/api/search/hello-world_test.123", out var parameters); + + // Assert + parameters["query"].Should().Be("hello-world_test.123"); + } + + [Fact] + public void IsMatch_ComplexRealWorldPath_Matches() + { + // Arrange + var matcher = new PathMatcher("/v1/organizations/{orgId}/projects/{projectId}/scans/{scanId}/vulnerabilities"); + + // Act + var result = matcher.IsMatch("/v1/organizations/acme-corp/projects/webapp/scans/scan-2024-001/vulnerabilities"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void TryMatch_ComplexRealWorldPath_ExtractsAllParameters() + { + // Arrange + var matcher = new PathMatcher("/v1/organizations/{orgId}/projects/{projectId}/scans/{scanId}"); + + // Act + matcher.TryMatch("/v1/organizations/acme-corp/projects/webapp/scans/scan-2024-001", out var parameters); + + // Assert + parameters["orgId"].Should().Be("acme-corp"); + parameters["projectId"].Should().Be("webapp"); + parameters["scanId"].Should().Be("scan-2024-001"); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/StellaOps.Router.Common.Tests.csproj b/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/StellaOps.Router.Common.Tests.csproj new file mode 100644 index 000000000..e89404f55 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Common.Tests/StellaOps.Router.Common.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + preview + enable + enable + true + + $(NoWarn);CA2255 + false + StellaOps.Router.Common.Tests + + false + + + + + + + + + + + + + + + + + + diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ConfigChangedEventArgsTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ConfigChangedEventArgsTests.cs new file mode 100644 index 000000000..b7bcbda61 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ConfigChangedEventArgsTests.cs @@ -0,0 +1,87 @@ +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for . +/// +public sealed class ConfigChangedEventArgsTests +{ + [Fact] + public void Constructor_SetsPreviousConfig() + { + // Arrange + var previous = new RouterConfig(); + var current = new RouterConfig(); + + // Act + var args = new ConfigChangedEventArgs(previous, current); + + // Assert + args.Previous.Should().BeSameAs(previous); + } + + [Fact] + public void Constructor_SetsCurrentConfig() + { + // Arrange + var previous = new RouterConfig(); + var current = new RouterConfig(); + + // Act + var args = new ConfigChangedEventArgs(previous, current); + + // Assert + args.Current.Should().BeSameAs(current); + } + + [Fact] + public void Constructor_SetsChangedAtToCurrentTime() + { + // Arrange + var previous = new RouterConfig(); + var current = new RouterConfig(); + var beforeCreate = DateTime.UtcNow; + + // Act + var args = new ConfigChangedEventArgs(previous, current); + var afterCreate = DateTime.UtcNow; + + // Assert + args.ChangedAt.Should().BeOnOrAfter(beforeCreate); + args.ChangedAt.Should().BeOnOrBefore(afterCreate); + } + + [Fact] + public void Constructor_DifferentConfigs_BothAccessible() + { + // Arrange + var previous = new RouterConfig + { + Routing = new RoutingOptions { LocalRegion = "us-west-1" } + }; + var current = new RouterConfig + { + Routing = new RoutingOptions { LocalRegion = "us-east-1" } + }; + + // Act + var args = new ConfigChangedEventArgs(previous, current); + + // Assert + args.Previous.Routing.LocalRegion.Should().Be("us-west-1"); + args.Current.Routing.LocalRegion.Should().Be("us-east-1"); + } + + [Fact] + public void ConfigChangedEventArgs_InheritsFromEventArgs() + { + // Arrange + var previous = new RouterConfig(); + var current = new RouterConfig(); + + // Act + var args = new ConfigChangedEventArgs(previous, current); + + // Assert + args.Should().BeAssignableTo(); + } +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ConfigValidationResultTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ConfigValidationResultTests.cs new file mode 100644 index 000000000..b50d0bbbb --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ConfigValidationResultTests.cs @@ -0,0 +1,190 @@ +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for . +/// +public sealed class ConfigValidationResultTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_Errors_DefaultsToEmptyList() + { + // Arrange & Act + var result = new ConfigValidationResult(); + + // Assert + result.Errors.Should().NotBeNull(); + result.Errors.Should().BeEmpty(); + } + + [Fact] + public void Constructor_Warnings_DefaultsToEmptyList() + { + // Arrange & Act + var result = new ConfigValidationResult(); + + // Assert + result.Warnings.Should().NotBeNull(); + result.Warnings.Should().BeEmpty(); + } + + #endregion + + #region IsValid Tests + + [Fact] + public void IsValid_NoErrors_ReturnsTrue() + { + // Arrange + var result = new ConfigValidationResult(); + + // Act & Assert + result.IsValid.Should().BeTrue(); + } + + [Fact] + public void IsValid_WithErrors_ReturnsFalse() + { + // Arrange + var result = new ConfigValidationResult(); + result.Errors.Add("Some error"); + + // Act & Assert + result.IsValid.Should().BeFalse(); + } + + [Fact] + public void IsValid_WithOnlyWarnings_ReturnsTrue() + { + // Arrange + var result = new ConfigValidationResult(); + result.Warnings.Add("Some warning"); + + // Act & Assert + result.IsValid.Should().BeTrue(); + } + + [Fact] + public void IsValid_WithErrorsAndWarnings_ReturnsFalse() + { + // Arrange + var result = new ConfigValidationResult(); + result.Errors.Add("Some error"); + result.Warnings.Add("Some warning"); + + // Act & Assert + result.IsValid.Should().BeFalse(); + } + + [Fact] + public void IsValid_MultipleErrors_ReturnsFalse() + { + // Arrange + var result = new ConfigValidationResult(); + result.Errors.Add("Error 1"); + result.Errors.Add("Error 2"); + result.Errors.Add("Error 3"); + + // Act & Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().HaveCount(3); + } + + #endregion + + #region Static Success Tests + + [Fact] + public void Success_ReturnsValidResult() + { + // Arrange & Act + var result = ConfigValidationResult.Success; + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + result.Warnings.Should().BeEmpty(); + } + + [Fact] + public void Success_ReturnsNewInstance() + { + // Arrange & Act + var result1 = ConfigValidationResult.Success; + var result2 = ConfigValidationResult.Success; + + // Assert - Should be different instances to allow mutation without affecting shared state + result1.Should().NotBeSameAs(result2); + } + + #endregion + + #region Errors Collection Tests + + [Fact] + public void Errors_CanBeModified() + { + // Arrange + var result = new ConfigValidationResult(); + + // Act + result.Errors.Add("Error 1"); + result.Errors.Add("Error 2"); + + // Assert + result.Errors.Should().HaveCount(2); + result.Errors.Should().Contain("Error 1"); + result.Errors.Should().Contain("Error 2"); + } + + [Fact] + public void Errors_CanBeInitialized() + { + // Arrange & Act + var result = new ConfigValidationResult + { + Errors = ["Error 1", "Error 2"] + }; + + // Assert + result.Errors.Should().HaveCount(2); + result.IsValid.Should().BeFalse(); + } + + #endregion + + #region Warnings Collection Tests + + [Fact] + public void Warnings_CanBeModified() + { + // Arrange + var result = new ConfigValidationResult(); + + // Act + result.Warnings.Add("Warning 1"); + result.Warnings.Add("Warning 2"); + + // Assert + result.Warnings.Should().HaveCount(2); + result.Warnings.Should().Contain("Warning 1"); + result.Warnings.Should().Contain("Warning 2"); + } + + [Fact] + public void Warnings_CanBeInitialized() + { + // Arrange & Act + var result = new ConfigValidationResult + { + Warnings = ["Warning 1", "Warning 2"] + }; + + // Assert + result.Warnings.Should().HaveCount(2); + result.IsValid.Should().BeTrue(); // Warnings don't affect validity + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigOptionsTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigOptionsTests.cs new file mode 100644 index 000000000..60ed023c4 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigOptionsTests.cs @@ -0,0 +1,153 @@ +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for . +/// +public sealed class RouterConfigOptionsTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_ConfigPath_DefaultsToNull() + { + // Arrange & Act + var options = new RouterConfigOptions(); + + // Assert + options.ConfigPath.Should().BeNull(); + } + + [Fact] + public void Constructor_EnvironmentVariablePrefix_DefaultsToStellaOpsRouter() + { + // Arrange & Act + var options = new RouterConfigOptions(); + + // Assert + options.EnvironmentVariablePrefix.Should().Be("STELLAOPS_ROUTER_"); + } + + [Fact] + public void Constructor_EnableHotReload_DefaultsToTrue() + { + // Arrange & Act + var options = new RouterConfigOptions(); + + // Assert + options.EnableHotReload.Should().BeTrue(); + } + + [Fact] + public void Constructor_DebounceInterval_DefaultsTo500Milliseconds() + { + // Arrange & Act + var options = new RouterConfigOptions(); + + // Assert + options.DebounceInterval.Should().Be(TimeSpan.FromMilliseconds(500)); + } + + [Fact] + public void Constructor_ThrowOnValidationError_DefaultsToFalse() + { + // Arrange & Act + var options = new RouterConfigOptions(); + + // Assert + options.ThrowOnValidationError.Should().BeFalse(); + } + + [Fact] + public void Constructor_ConfigurationSection_DefaultsToRouter() + { + // Arrange & Act + var options = new RouterConfigOptions(); + + // Assert + options.ConfigurationSection.Should().Be("Router"); + } + + #endregion + + #region Property Assignment Tests + + [Fact] + public void ConfigPath_CanBeSet() + { + // Arrange + var options = new RouterConfigOptions(); + + // Act + options.ConfigPath = "/etc/stellaops/router.yaml"; + + // Assert + options.ConfigPath.Should().Be("/etc/stellaops/router.yaml"); + } + + [Fact] + public void EnvironmentVariablePrefix_CanBeSet() + { + // Arrange + var options = new RouterConfigOptions(); + + // Act + options.EnvironmentVariablePrefix = "CUSTOM_PREFIX_"; + + // Assert + options.EnvironmentVariablePrefix.Should().Be("CUSTOM_PREFIX_"); + } + + [Fact] + public void EnableHotReload_CanBeSet() + { + // Arrange + var options = new RouterConfigOptions(); + + // Act + options.EnableHotReload = false; + + // Assert + options.EnableHotReload.Should().BeFalse(); + } + + [Fact] + public void DebounceInterval_CanBeSet() + { + // Arrange + var options = new RouterConfigOptions(); + + // Act + options.DebounceInterval = TimeSpan.FromSeconds(2); + + // Assert + options.DebounceInterval.Should().Be(TimeSpan.FromSeconds(2)); + } + + [Fact] + public void ThrowOnValidationError_CanBeSet() + { + // Arrange + var options = new RouterConfigOptions(); + + // Act + options.ThrowOnValidationError = true; + + // Assert + options.ThrowOnValidationError.Should().BeTrue(); + } + + [Fact] + public void ConfigurationSection_CanBeSet() + { + // Arrange + var options = new RouterConfigOptions(); + + // Act + options.ConfigurationSection = "CustomSection"; + + // Assert + options.ConfigurationSection.Should().Be("CustomSection"); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigProviderTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigProviderTests.cs new file mode 100644 index 000000000..ac8705216 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigProviderTests.cs @@ -0,0 +1,536 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for and configuration validation. +/// +public sealed class RouterConfigProviderTests : IDisposable +{ + private readonly ILogger _logger; + private RouterConfigProvider? _provider; + + public RouterConfigProviderTests() + { + _logger = NullLogger.Instance; + } + + public void Dispose() + { + _provider?.Dispose(); + } + + private RouterConfigProvider CreateProvider(RouterConfigOptions? options = null) + { + var opts = Options.Create(options ?? new RouterConfigOptions { EnableHotReload = false }); + _provider = new RouterConfigProvider(opts, _logger); + return _provider; + } + + #region Constructor Tests + + [Fact] + public void Constructor_InitializesCurrentConfig() + { + // Arrange & Act + var provider = CreateProvider(); + + // Assert + provider.Current.Should().NotBeNull(); + } + + [Fact] + public void Constructor_ExposesOptions() + { + // Arrange + var options = new RouterConfigOptions + { + ConfigPath = "/test/path.yaml", + EnableHotReload = false + }; + + // Act + var provider = CreateProvider(options); + + // Assert + provider.Options.Should().NotBeNull(); + provider.Options.ConfigPath.Should().Be("/test/path.yaml"); + } + + [Fact] + public void Constructor_WithHotReloadDisabled_DoesNotThrow() + { + // Arrange + var options = new RouterConfigOptions { EnableHotReload = false }; + + // Act + var action = () => CreateProvider(options); + + // Assert + action.Should().NotThrow(); + } + + #endregion + + #region Validate Tests - PayloadLimits + + [Fact] + public void Validate_ValidConfig_ReturnsIsValid() + { + // Arrange + var provider = CreateProvider(); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Fact] + public void Validate_ZeroMaxRequestBytesPerCall_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerCall = 0 }; + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerCall")); + } + + [Fact] + public void Validate_NegativeMaxRequestBytesPerCall_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerCall = -1 }; + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerCall")); + } + + [Fact] + public void Validate_ZeroMaxRequestBytesPerConnection_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerConnection = 0 }; + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("MaxRequestBytesPerConnection")); + } + + [Fact] + public void Validate_ZeroMaxAggregateInflightBytes_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.PayloadLimits = new PayloadLimits { MaxAggregateInflightBytes = 0 }; + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("MaxAggregateInflightBytes")); + } + + [Fact] + public void Validate_MaxCallBytesLargerThanConnectionBytes_ReturnsWarning() + { + // Arrange + var provider = CreateProvider(); + provider.Current.PayloadLimits = new PayloadLimits + { + MaxRequestBytesPerCall = 100 * 1024 * 1024, + MaxRequestBytesPerConnection = 10 * 1024 * 1024, + MaxAggregateInflightBytes = 1024 * 1024 * 1024 + }; + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeTrue(); // Warnings don't fail validation + result.Warnings.Should().Contain(w => w.Contains("MaxRequestBytesPerCall") && w.Contains("MaxRequestBytesPerConnection")); + } + + #endregion + + #region Validate Tests - RoutingOptions + + [Fact] + public void Validate_ZeroDefaultTimeout_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Routing.DefaultTimeout = TimeSpan.Zero; + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("DefaultTimeout")); + } + + [Fact] + public void Validate_NegativeDefaultTimeout_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Routing.DefaultTimeout = TimeSpan.FromSeconds(-1); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("DefaultTimeout")); + } + + #endregion + + #region Validate Tests - Services + + [Fact] + public void Validate_EmptyServiceName_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig { ServiceName = "" }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("Service name cannot be empty")); + } + + [Fact] + public void Validate_WhitespaceServiceName_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig { ServiceName = " " }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("Service name cannot be empty")); + } + + [Fact] + public void Validate_DuplicateServiceNames_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig { ServiceName = "my-service" }); + provider.Current.Services.Add(new ServiceConfig { ServiceName = "my-service" }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("Duplicate service name")); + } + + [Fact] + public void Validate_DuplicateServiceNamesCaseInsensitive_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig { ServiceName = "MyService" }); + provider.Current.Services.Add(new ServiceConfig { ServiceName = "myservice" }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("Duplicate service name")); + } + + [Fact] + public void Validate_EndpointEmptyMethod_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig + { + ServiceName = "test", + Endpoints = [new EndpointConfig { Method = "", Path = "/test" }] + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("endpoint method cannot be empty")); + } + + [Fact] + public void Validate_EndpointEmptyPath_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig + { + ServiceName = "test", + Endpoints = [new EndpointConfig { Method = "GET", Path = "" }] + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("endpoint path cannot be empty")); + } + + [Fact] + public void Validate_EndpointNonPositiveTimeout_ReturnsWarning() + { + // Arrange + var provider = CreateProvider(); + provider.Current.Services.Add(new ServiceConfig + { + ServiceName = "test", + Endpoints = [new EndpointConfig { Method = "GET", Path = "/test", DefaultTimeout = TimeSpan.Zero }] + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeTrue(); // Warnings don't fail validation + result.Warnings.Should().Contain(w => w.Contains("non-positive timeout")); + } + + #endregion + + #region Validate Tests - StaticInstances + + [Fact] + public void Validate_StaticInstanceEmptyServiceName_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.StaticInstances.Add(new StaticInstanceConfig + { + ServiceName = "", + Version = "1.0", + Host = "localhost", + Port = 8080 + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("Static instance service name cannot be empty")); + } + + [Fact] + public void Validate_StaticInstanceEmptyHost_ReturnsError() + { + // Arrange + var provider = CreateProvider(); + provider.Current.StaticInstances.Add(new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "", + Port = 8080 + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("host cannot be empty")); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(65536)] + [InlineData(70000)] + public void Validate_StaticInstanceInvalidPort_ReturnsError(int port) + { + // Arrange + var provider = CreateProvider(); + provider.Current.StaticInstances.Add(new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = port + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.Contains("port must be between 1 and 65535")); + } + + [Theory] + [InlineData(1)] + [InlineData(80)] + [InlineData(443)] + [InlineData(8080)] + [InlineData(65535)] + public void Validate_StaticInstanceValidPort_Succeeds(int port) + { + // Arrange + var provider = CreateProvider(); + provider.Current.StaticInstances.Add(new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = port + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-100)] + public void Validate_StaticInstanceNonPositiveWeight_ReturnsWarning(int weight) + { + // Arrange + var provider = CreateProvider(); + provider.Current.StaticInstances.Add(new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080, + Weight = weight + }); + + // Act + var result = provider.Validate(); + + // Assert + result.IsValid.Should().BeTrue(); // Warnings don't fail validation + result.Warnings.Should().Contain(w => w.Contains("weight should be positive")); + } + + #endregion + + #region ReloadAsync Tests + + [Fact] + public async Task ReloadAsync_ValidConfig_UpdatesCurrentConfig() + { + // Arrange + var provider = CreateProvider(); + + // Act + await provider.ReloadAsync(); + + // Assert - Config should be reloaded (same content in this case since no file) + provider.Current.Should().NotBeNull(); + } + + [Fact] + public async Task ReloadAsync_InvalidConfig_ThrowsConfigurationException() + { + // Arrange + var provider = CreateProvider(); + provider.Current.PayloadLimits = new PayloadLimits { MaxRequestBytesPerCall = 0 }; + + // Act & Assert + await Assert.ThrowsAsync(() => provider.ReloadAsync()); + } + + [Fact] + public async Task ReloadAsync_Cancelled_ThrowsOperationCanceledException() + { + // Arrange + var provider = CreateProvider(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync(() => provider.ReloadAsync(cts.Token)); + } + + #endregion + + #region ConfigurationChanged Event Tests + + [Fact] + public async Task ReloadAsync_RaisesConfigurationChangedEvent() + { + // Arrange + var provider = CreateProvider(); + ConfigChangedEventArgs? eventArgs = null; + provider.ConfigurationChanged += (_, args) => eventArgs = args; + + // Act + await provider.ReloadAsync(); + + // Assert + eventArgs.Should().NotBeNull(); + eventArgs!.Previous.Should().NotBeNull(); + eventArgs.Current.Should().NotBeNull(); + eventArgs.ChangedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + } + + #endregion + + #region Dispose Tests + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + // Arrange + var provider = CreateProvider(); + + // Act + var action = () => + { + provider.Dispose(); + provider.Dispose(); + provider.Dispose(); + }; + + // Assert + action.Should().NotThrow(); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigTests.cs new file mode 100644 index 000000000..a53b67273 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RouterConfigTests.cs @@ -0,0 +1,279 @@ +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for . +/// +public sealed class RouterConfigTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_PayloadLimits_DefaultsToNewInstance() + { + // Arrange & Act + var config = new RouterConfig(); + + // Assert + config.PayloadLimits.Should().NotBeNull(); + } + + [Fact] + public void Constructor_Routing_DefaultsToNewInstance() + { + // Arrange & Act + var config = new RouterConfig(); + + // Assert + config.Routing.Should().NotBeNull(); + } + + [Fact] + public void Constructor_Services_DefaultsToEmptyList() + { + // Arrange & Act + var config = new RouterConfig(); + + // Assert + config.Services.Should().NotBeNull(); + config.Services.Should().BeEmpty(); + } + + [Fact] + public void Constructor_StaticInstances_DefaultsToEmptyList() + { + // Arrange & Act + var config = new RouterConfig(); + + // Assert + config.StaticInstances.Should().NotBeNull(); + config.StaticInstances.Should().BeEmpty(); + } + + #endregion + + #region PayloadLimits Tests + + [Fact] + public void PayloadLimits_HasDefaultValues() + { + // Arrange & Act + var config = new RouterConfig(); + + // Assert + config.PayloadLimits.MaxRequestBytesPerCall.Should().Be(10 * 1024 * 1024); // 10 MB + config.PayloadLimits.MaxRequestBytesPerConnection.Should().Be(100 * 1024 * 1024); // 100 MB + config.PayloadLimits.MaxAggregateInflightBytes.Should().Be(1024 * 1024 * 1024); // 1 GB + } + + [Fact] + public void PayloadLimits_CanBeSet() + { + // Arrange + var config = new RouterConfig(); + + // Act + config.PayloadLimits = new PayloadLimits + { + MaxRequestBytesPerCall = 5 * 1024 * 1024, + MaxRequestBytesPerConnection = 50 * 1024 * 1024, + MaxAggregateInflightBytes = 500 * 1024 * 1024 + }; + + // Assert + config.PayloadLimits.MaxRequestBytesPerCall.Should().Be(5 * 1024 * 1024); + config.PayloadLimits.MaxRequestBytesPerConnection.Should().Be(50 * 1024 * 1024); + config.PayloadLimits.MaxAggregateInflightBytes.Should().Be(500 * 1024 * 1024); + } + + #endregion + + #region Routing Tests + + [Fact] + public void Routing_HasDefaultValues() + { + // Arrange & Act + var config = new RouterConfig(); + + // Assert + config.Routing.LocalRegion.Should().Be("default"); + config.Routing.TieBreaker.Should().Be(TieBreakerStrategy.RoundRobin); + config.Routing.PreferLocalRegion.Should().BeTrue(); + config.Routing.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); + } + + [Fact] + public void Routing_CanBeSet() + { + // Arrange + var config = new RouterConfig(); + + // Act + config.Routing = new RoutingOptions + { + LocalRegion = "us-east-1", + TieBreaker = TieBreakerStrategy.LeastLoaded, + PreferLocalRegion = false, + DefaultTimeout = TimeSpan.FromMinutes(2), + NeighborRegions = ["us-west-1", "eu-west-1"] + }; + + // Assert + config.Routing.LocalRegion.Should().Be("us-east-1"); + config.Routing.TieBreaker.Should().Be(TieBreakerStrategy.LeastLoaded); + config.Routing.PreferLocalRegion.Should().BeFalse(); + config.Routing.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(2)); + config.Routing.NeighborRegions.Should().HaveCount(2); + } + + #endregion + + #region Services Tests + + [Fact] + public void Services_CanAddServices() + { + // Arrange + var config = new RouterConfig(); + + // Act + config.Services.Add(new ServiceConfig { ServiceName = "service-a" }); + config.Services.Add(new ServiceConfig { ServiceName = "service-b" }); + + // Assert + config.Services.Should().HaveCount(2); + config.Services[0].ServiceName.Should().Be("service-a"); + config.Services[1].ServiceName.Should().Be("service-b"); + } + + [Fact] + public void Services_CanBeInitialized() + { + // Arrange & Act + var config = new RouterConfig + { + Services = + [ + new ServiceConfig { ServiceName = "auth" }, + new ServiceConfig { ServiceName = "users" }, + new ServiceConfig { ServiceName = "orders" } + ] + }; + + // Assert + config.Services.Should().HaveCount(3); + } + + #endregion + + #region StaticInstances Tests + + [Fact] + public void StaticInstances_CanAddInstances() + { + // Arrange + var config = new RouterConfig(); + + // Act + config.StaticInstances.Add(new StaticInstanceConfig + { + ServiceName = "legacy-service", + Version = "1.0", + Host = "legacy.internal", + Port = 9000 + }); + + // Assert + config.StaticInstances.Should().HaveCount(1); + config.StaticInstances[0].ServiceName.Should().Be("legacy-service"); + } + + [Fact] + public void StaticInstances_CanBeInitialized() + { + // Arrange & Act + var config = new RouterConfig + { + StaticInstances = + [ + new StaticInstanceConfig + { + ServiceName = "db-proxy", + Version = "2.0", + Host = "db-proxy-1.internal", + Port = 5432 + }, + new StaticInstanceConfig + { + ServiceName = "db-proxy", + Version = "2.0", + Host = "db-proxy-2.internal", + Port = 5432 + } + ] + }; + + // Assert + config.StaticInstances.Should().HaveCount(2); + } + + #endregion + + #region Complete Configuration Tests + + [Fact] + public void CompleteConfiguration_Works() + { + // Arrange & Act + var config = new RouterConfig + { + PayloadLimits = new PayloadLimits + { + MaxRequestBytesPerCall = 1024 * 1024, + MaxRequestBytesPerConnection = 10 * 1024 * 1024, + MaxAggregateInflightBytes = 100 * 1024 * 1024 + }, + Routing = new RoutingOptions + { + LocalRegion = "us-east-1", + NeighborRegions = ["us-west-1"], + TieBreaker = TieBreakerStrategy.ConsistentHash, + PreferLocalRegion = true, + DefaultTimeout = TimeSpan.FromSeconds(60) + }, + Services = + [ + new ServiceConfig + { + ServiceName = "api-gateway", + DefaultVersion = "1.0.0", + Endpoints = + [ + new EndpointConfig { Method = "GET", Path = "/health" } + ] + } + ], + StaticInstances = + [ + new StaticInstanceConfig + { + ServiceName = "api-gateway", + Version = "1.0.0", + Host = "api-1.internal", + Port = 8080, + Weight = 100 + } + ] + }; + + // Assert + config.PayloadLimits.MaxRequestBytesPerCall.Should().Be(1024 * 1024); + config.Routing.LocalRegion.Should().Be("us-east-1"); + config.Services.Should().HaveCount(1); + config.StaticInstances.Should().HaveCount(1); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RoutingOptionsTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RoutingOptionsTests.cs new file mode 100644 index 000000000..4028b5516 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/RoutingOptionsTests.cs @@ -0,0 +1,179 @@ +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for and . +/// +public sealed class RoutingOptionsTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_LocalRegion_DefaultsToDefault() + { + // Arrange & Act + var options = new RoutingOptions(); + + // Assert + options.LocalRegion.Should().Be("default"); + } + + [Fact] + public void Constructor_NeighborRegions_DefaultsToEmptyList() + { + // Arrange & Act + var options = new RoutingOptions(); + + // Assert + options.NeighborRegions.Should().NotBeNull(); + options.NeighborRegions.Should().BeEmpty(); + } + + [Fact] + public void Constructor_TieBreaker_DefaultsToRoundRobin() + { + // Arrange & Act + var options = new RoutingOptions(); + + // Assert + options.TieBreaker.Should().Be(TieBreakerStrategy.RoundRobin); + } + + [Fact] + public void Constructor_PreferLocalRegion_DefaultsToTrue() + { + // Arrange & Act + var options = new RoutingOptions(); + + // Assert + options.PreferLocalRegion.Should().BeTrue(); + } + + [Fact] + public void Constructor_DefaultTimeout_DefaultsTo30Seconds() + { + // Arrange & Act + var options = new RoutingOptions(); + + // Assert + options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); + } + + #endregion + + #region Property Assignment Tests + + [Fact] + public void LocalRegion_CanBeSet() + { + // Arrange + var options = new RoutingOptions(); + + // Act + options.LocalRegion = "us-east-1"; + + // Assert + options.LocalRegion.Should().Be("us-east-1"); + } + + [Fact] + public void NeighborRegions_CanBeSet() + { + // Arrange + var options = new RoutingOptions(); + + // Act + options.NeighborRegions = ["us-west-1", "eu-west-1"]; + + // Assert + options.NeighborRegions.Should().HaveCount(2); + options.NeighborRegions.Should().Contain("us-west-1"); + options.NeighborRegions.Should().Contain("eu-west-1"); + } + + [Theory] + [InlineData(TieBreakerStrategy.RoundRobin)] + [InlineData(TieBreakerStrategy.Random)] + [InlineData(TieBreakerStrategy.LeastLoaded)] + [InlineData(TieBreakerStrategy.ConsistentHash)] + public void TieBreaker_CanBeSetToAllStrategies(TieBreakerStrategy strategy) + { + // Arrange + var options = new RoutingOptions(); + + // Act + options.TieBreaker = strategy; + + // Assert + options.TieBreaker.Should().Be(strategy); + } + + [Fact] + public void PreferLocalRegion_CanBeSet() + { + // Arrange + var options = new RoutingOptions(); + + // Act + options.PreferLocalRegion = false; + + // Assert + options.PreferLocalRegion.Should().BeFalse(); + } + + [Fact] + public void DefaultTimeout_CanBeSet() + { + // Arrange + var options = new RoutingOptions(); + + // Act + options.DefaultTimeout = TimeSpan.FromMinutes(5); + + // Assert + options.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5)); + } + + #endregion + + #region TieBreakerStrategy Enum Tests + + [Fact] + public void TieBreakerStrategy_HasFourValues() + { + // Arrange & Act + var values = Enum.GetValues(); + + // Assert + values.Should().HaveCount(4); + } + + [Fact] + public void TieBreakerStrategy_RoundRobin_HasValueZero() + { + // Arrange & Act & Assert + ((int)TieBreakerStrategy.RoundRobin).Should().Be(0); + } + + [Fact] + public void TieBreakerStrategy_Random_HasValueOne() + { + // Arrange & Act & Assert + ((int)TieBreakerStrategy.Random).Should().Be(1); + } + + [Fact] + public void TieBreakerStrategy_LeastLoaded_HasValueTwo() + { + // Arrange & Act & Assert + ((int)TieBreakerStrategy.LeastLoaded).Should().Be(2); + } + + [Fact] + public void TieBreakerStrategy_ConsistentHash_HasValueThree() + { + // Arrange & Act & Assert + ((int)TieBreakerStrategy.ConsistentHash).Should().Be(3); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ServiceConfigTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ServiceConfigTests.cs new file mode 100644 index 000000000..155da6b95 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/ServiceConfigTests.cs @@ -0,0 +1,253 @@ +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for and . +/// +public sealed class ServiceConfigTests +{ + #region ServiceConfig Default Values Tests + + [Fact] + public void ServiceConfig_DefaultVersion_DefaultsToNull() + { + // Arrange & Act + var config = new ServiceConfig { ServiceName = "test" }; + + // Assert + config.DefaultVersion.Should().BeNull(); + } + + [Fact] + public void ServiceConfig_DefaultTransport_DefaultsToTcp() + { + // Arrange & Act + var config = new ServiceConfig { ServiceName = "test" }; + + // Assert + config.DefaultTransport.Should().Be(TransportType.Tcp); + } + + [Fact] + public void ServiceConfig_Endpoints_DefaultsToEmptyList() + { + // Arrange & Act + var config = new ServiceConfig { ServiceName = "test" }; + + // Assert + config.Endpoints.Should().NotBeNull(); + config.Endpoints.Should().BeEmpty(); + } + + #endregion + + #region ServiceConfig Property Assignment Tests + + [Fact] + public void ServiceConfig_ServiceName_CanBeSet() + { + // Arrange & Act + var config = new ServiceConfig { ServiceName = "my-service" }; + + // Assert + config.ServiceName.Should().Be("my-service"); + } + + [Fact] + public void ServiceConfig_DefaultVersion_CanBeSet() + { + // Arrange + var config = new ServiceConfig { ServiceName = "test" }; + + // Act + config.DefaultVersion = "1.0.0"; + + // Assert + config.DefaultVersion.Should().Be("1.0.0"); + } + + [Theory] + [InlineData(TransportType.Tcp)] + [InlineData(TransportType.Certificate)] + [InlineData(TransportType.Udp)] + [InlineData(TransportType.InMemory)] + [InlineData(TransportType.RabbitMq)] + public void ServiceConfig_DefaultTransport_CanBeSetToAllTypes(TransportType transport) + { + // Arrange + var config = new ServiceConfig { ServiceName = "test" }; + + // Act + config.DefaultTransport = transport; + + // Assert + config.DefaultTransport.Should().Be(transport); + } + + [Fact] + public void ServiceConfig_Endpoints_CanAddEndpoints() + { + // Arrange + var config = new ServiceConfig { ServiceName = "test" }; + + // Act + config.Endpoints.Add(new EndpointConfig { Method = "GET", Path = "/api/health" }); + config.Endpoints.Add(new EndpointConfig { Method = "POST", Path = "/api/data" }); + + // Assert + config.Endpoints.Should().HaveCount(2); + } + + #endregion + + #region EndpointConfig Default Values Tests + + [Fact] + public void EndpointConfig_DefaultTimeout_DefaultsToNull() + { + // Arrange & Act + var endpoint = new EndpointConfig { Method = "GET", Path = "/" }; + + // Assert + endpoint.DefaultTimeout.Should().BeNull(); + } + + [Fact] + public void EndpointConfig_SupportsStreaming_DefaultsToFalse() + { + // Arrange & Act + var endpoint = new EndpointConfig { Method = "GET", Path = "/" }; + + // Assert + endpoint.SupportsStreaming.Should().BeFalse(); + } + + [Fact] + public void EndpointConfig_RequiringClaims_DefaultsToEmptyList() + { + // Arrange & Act + var endpoint = new EndpointConfig { Method = "GET", Path = "/" }; + + // Assert + endpoint.RequiringClaims.Should().NotBeNull(); + endpoint.RequiringClaims.Should().BeEmpty(); + } + + #endregion + + #region EndpointConfig Property Assignment Tests + + [Fact] + public void EndpointConfig_Method_CanBeSet() + { + // Arrange & Act + var endpoint = new EndpointConfig { Method = "DELETE", Path = "/" }; + + // Assert + endpoint.Method.Should().Be("DELETE"); + } + + [Fact] + public void EndpointConfig_Path_CanBeSet() + { + // Arrange & Act + var endpoint = new EndpointConfig { Method = "GET", Path = "/api/users/{id}" }; + + // Assert + endpoint.Path.Should().Be("/api/users/{id}"); + } + + [Fact] + public void EndpointConfig_DefaultTimeout_CanBeSet() + { + // Arrange + var endpoint = new EndpointConfig { Method = "GET", Path = "/" }; + + // Act + endpoint.DefaultTimeout = TimeSpan.FromSeconds(60); + + // Assert + endpoint.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(60)); + } + + [Fact] + public void EndpointConfig_SupportsStreaming_CanBeSet() + { + // Arrange + var endpoint = new EndpointConfig { Method = "GET", Path = "/" }; + + // Act + endpoint.SupportsStreaming = true; + + // Assert + endpoint.SupportsStreaming.Should().BeTrue(); + } + + [Fact] + public void EndpointConfig_RequiringClaims_CanAddClaims() + { + // Arrange + var endpoint = new EndpointConfig { Method = "GET", Path = "/" }; + + // Act + endpoint.RequiringClaims.Add(new ClaimRequirement { Type = "role", Value = "admin" }); + endpoint.RequiringClaims.Add(new ClaimRequirement { Type = "permission", Value = "read" }); + + // Assert + endpoint.RequiringClaims.Should().HaveCount(2); + } + + #endregion + + #region Complex Configuration Tests + + [Fact] + public void ServiceConfig_CompleteConfiguration_Works() + { + // Arrange & Act + var config = new ServiceConfig + { + ServiceName = "user-service", + DefaultVersion = "2.0.0", + DefaultTransport = TransportType.Certificate, + Endpoints = + [ + new EndpointConfig + { + Method = "GET", + Path = "/api/users/{id}", + DefaultTimeout = TimeSpan.FromSeconds(10), + SupportsStreaming = false, + RequiringClaims = [new ClaimRequirement { Type = "role", Value = "user" }] + }, + new EndpointConfig + { + Method = "POST", + Path = "/api/users", + DefaultTimeout = TimeSpan.FromSeconds(30), + SupportsStreaming = false, + RequiringClaims = [new ClaimRequirement { Type = "role", Value = "admin" }] + }, + new EndpointConfig + { + Method = "GET", + Path = "/api/users/stream", + DefaultTimeout = TimeSpan.FromMinutes(5), + SupportsStreaming = true + } + ] + }; + + // Assert + config.ServiceName.Should().Be("user-service"); + config.DefaultVersion.Should().Be("2.0.0"); + config.DefaultTransport.Should().Be(TransportType.Certificate); + config.Endpoints.Should().HaveCount(3); + config.Endpoints[0].RequiringClaims.Should().HaveCount(1); + config.Endpoints[2].SupportsStreaming.Should().BeTrue(); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/StaticInstanceConfigTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/StaticInstanceConfigTests.cs new file mode 100644 index 000000000..9eddad66c --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/StaticInstanceConfigTests.cs @@ -0,0 +1,311 @@ +using StellaOps.Router.Common.Enums; + +namespace StellaOps.Router.Config.Tests; + +/// +/// Unit tests for . +/// +public sealed class StaticInstanceConfigTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_Region_DefaultsToDefault() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Assert + config.Region.Should().Be("default"); + } + + [Fact] + public void Constructor_Transport_DefaultsToTcp() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Assert + config.Transport.Should().Be(TransportType.Tcp); + } + + [Fact] + public void Constructor_Weight_DefaultsTo100() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Assert + config.Weight.Should().Be(100); + } + + [Fact] + public void Constructor_Metadata_DefaultsToEmptyDictionary() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Assert + config.Metadata.Should().NotBeNull(); + config.Metadata.Should().BeEmpty(); + } + + #endregion + + #region Required Properties Tests + + [Fact] + public void ServiceName_IsRequired() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "required-service", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Assert + config.ServiceName.Should().Be("required-service"); + } + + [Fact] + public void Version_IsRequired() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "2.3.4", + Host = "localhost", + Port = 8080 + }; + + // Assert + config.Version.Should().Be("2.3.4"); + } + + [Fact] + public void Host_IsRequired() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "192.168.1.100", + Port = 8080 + }; + + // Assert + config.Host.Should().Be("192.168.1.100"); + } + + [Fact] + public void Port_IsRequired() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 443 + }; + + // Assert + config.Port.Should().Be(443); + } + + #endregion + + #region Property Assignment Tests + + [Fact] + public void Region_CanBeSet() + { + // Arrange + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Act + config.Region = "us-west-2"; + + // Assert + config.Region.Should().Be("us-west-2"); + } + + [Theory] + [InlineData(TransportType.Tcp)] + [InlineData(TransportType.Certificate)] + [InlineData(TransportType.Udp)] + [InlineData(TransportType.InMemory)] + [InlineData(TransportType.RabbitMq)] + public void Transport_CanBeSetToAllTypes(TransportType transport) + { + // Arrange + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Act + config.Transport = transport; + + // Assert + config.Transport.Should().Be(transport); + } + + [Theory] + [InlineData(1)] + [InlineData(50)] + [InlineData(100)] + [InlineData(200)] + [InlineData(1000)] + public void Weight_CanBeSet(int weight) + { + // Arrange + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Act + config.Weight = weight; + + // Assert + config.Weight.Should().Be(weight); + } + + [Fact] + public void Metadata_CanAddEntries() + { + // Arrange + var config = new StaticInstanceConfig + { + ServiceName = "test", + Version = "1.0", + Host = "localhost", + Port = 8080 + }; + + // Act + config.Metadata["environment"] = "production"; + config.Metadata["cluster"] = "primary"; + + // Assert + config.Metadata.Should().HaveCount(2); + config.Metadata["environment"].Should().Be("production"); + config.Metadata["cluster"].Should().Be("primary"); + } + + #endregion + + #region Complex Configuration Tests + + [Fact] + public void CompleteConfiguration_Works() + { + // Arrange & Act + var config = new StaticInstanceConfig + { + ServiceName = "user-service", + Version = "3.2.1", + Region = "eu-central-1", + Host = "user-svc.internal.example.com", + Port = 8443, + Transport = TransportType.Certificate, + Weight = 150, + Metadata = new Dictionary + { + ["datacenter"] = "dc1", + ["rack"] = "rack-42", + ["shard"] = "primary" + } + }; + + // Assert + config.ServiceName.Should().Be("user-service"); + config.Version.Should().Be("3.2.1"); + config.Region.Should().Be("eu-central-1"); + config.Host.Should().Be("user-svc.internal.example.com"); + config.Port.Should().Be(8443); + config.Transport.Should().Be(TransportType.Certificate); + config.Weight.Should().Be(150); + config.Metadata.Should().HaveCount(3); + } + + [Fact] + public void MultipleInstances_CanHaveDifferentWeights() + { + // Arrange & Act + var primary = new StaticInstanceConfig + { + ServiceName = "api", + Version = "1.0", + Host = "primary.example.com", + Port = 8080, + Weight = 200 + }; + + var secondary = new StaticInstanceConfig + { + ServiceName = "api", + Version = "1.0", + Host = "secondary.example.com", + Port = 8080, + Weight = 100 + }; + + var tertiary = new StaticInstanceConfig + { + ServiceName = "api", + Version = "1.0", + Host = "tertiary.example.com", + Port = 8080, + Weight = 50 + }; + + // Assert + primary.Weight.Should().BeGreaterThan(secondary.Weight); + secondary.Weight.Should().BeGreaterThan(tertiary.Weight); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/StellaOps.Router.Config.Tests.csproj b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/StellaOps.Router.Config.Tests.csproj new file mode 100644 index 000000000..1a7ff696b --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Config.Tests/StellaOps.Router.Config.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + preview + enable + enable + true + + $(NoWarn);CA2255 + false + StellaOps.Router.Config.Tests + + false + + + + + + + + + + + + + + + + + + diff --git a/src/__Libraries/__Tests/StellaOps.Router.Testing/Factories/TestFrameFactory.cs b/src/__Libraries/__Tests/StellaOps.Router.Testing/Factories/TestFrameFactory.cs new file mode 100644 index 000000000..f3d2db450 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Testing/Factories/TestFrameFactory.cs @@ -0,0 +1,210 @@ +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Frames; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Testing.Factories; + +/// +/// Factory for creating test frames with sensible defaults. +/// +public static class TestFrameFactory +{ + /// + /// Creates a request frame with the specified payload. + /// + public static Frame CreateRequestFrame( + byte[]? payload = null, + string? correlationId = null, + FrameType frameType = FrameType.Request) + { + return new Frame + { + Type = frameType, + CorrelationId = correlationId ?? Guid.NewGuid().ToString("N"), + Payload = payload ?? Array.Empty() + }; + } + + /// + /// Creates a response frame for the given correlation ID. + /// + public static Frame CreateResponseFrame( + string correlationId, + byte[]? payload = null) + { + return new Frame + { + Type = FrameType.Response, + CorrelationId = correlationId, + Payload = payload ?? Array.Empty() + }; + } + + /// + /// Creates a hello frame for service registration. + /// + public static Frame CreateHelloFrame( + string serviceName = "test-service", + string version = "1.0.0", + string region = "test", + string instanceId = "test-instance", + IReadOnlyList? endpoints = null) + { + var helloPayload = new HelloPayload + { + Instance = new InstanceDescriptor + { + InstanceId = instanceId, + ServiceName = serviceName, + Version = version, + Region = region + }, + Endpoints = endpoints ?? [] + }; + + return new Frame + { + Type = FrameType.Hello, + CorrelationId = Guid.NewGuid().ToString("N"), + Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(helloPayload) + }; + } + + /// + /// Creates a heartbeat frame. + /// + public static Frame CreateHeartbeatFrame( + string instanceId = "test-instance", + InstanceHealthStatus status = InstanceHealthStatus.Healthy, + int inFlightRequestCount = 0, + double errorRate = 0.0) + { + var heartbeatPayload = new HeartbeatPayload + { + InstanceId = instanceId, + Status = status, + InFlightRequestCount = inFlightRequestCount, + ErrorRate = errorRate, + TimestampUtc = DateTime.UtcNow + }; + + return new Frame + { + Type = FrameType.Heartbeat, + CorrelationId = Guid.NewGuid().ToString("N"), + Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(heartbeatPayload) + }; + } + + /// + /// Creates a cancel frame for the given correlation ID. + /// + public static Frame CreateCancelFrame( + string correlationId, + string? reason = null) + { + var cancelPayload = new CancelPayload + { + Reason = reason ?? CancelReasons.Timeout + }; + + return new Frame + { + Type = FrameType.Cancel, + CorrelationId = correlationId, + Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(cancelPayload) + }; + } + + /// + /// Creates a frame with a specific payload size for testing limits. + /// + public static Frame CreateFrameWithPayloadSize(int payloadSize) + { + var payload = new byte[payloadSize]; + Random.Shared.NextBytes(payload); + + return new Frame + { + Type = FrameType.Request, + CorrelationId = Guid.NewGuid().ToString("N"), + Payload = payload + }; + } + + /// + /// Creates a request frame from JSON content. + /// + public static RequestFrame CreateTypedRequestFrame( + T request, + string method = "POST", + string path = "/test", + Dictionary? headers = null) + { + return new RequestFrame + { + RequestId = Guid.NewGuid().ToString("N"), + CorrelationId = Guid.NewGuid().ToString("N"), + Method = method, + Path = path, + Headers = headers ?? new Dictionary(), + Payload = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(request) + }; + } + + /// + /// Creates an endpoint descriptor for testing. + /// + public static EndpointDescriptor CreateEndpointDescriptor( + string method = "GET", + string path = "/test", + string serviceName = "test-service", + string version = "1.0.0", + int timeoutSeconds = 30, + bool supportsStreaming = false, + IReadOnlyList? requiringClaims = null) + { + return new EndpointDescriptor + { + Method = method, + Path = path, + ServiceName = serviceName, + Version = version, + DefaultTimeout = TimeSpan.FromSeconds(timeoutSeconds), + SupportsStreaming = supportsStreaming, + RequiringClaims = requiringClaims ?? [] + }; + } + + /// + /// Creates an instance descriptor for testing. + /// + public static InstanceDescriptor CreateInstanceDescriptor( + string instanceId = "test-instance", + string serviceName = "test-service", + string version = "1.0.0", + string region = "test") + { + return new InstanceDescriptor + { + InstanceId = instanceId, + ServiceName = serviceName, + Version = version, + Region = region + }; + } + + /// + /// Creates a claim requirement for testing. + /// + public static ClaimRequirement CreateClaimRequirement( + string type, + string? value = null) + { + return new ClaimRequirement + { + Type = type, + Value = value + }; + } +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Testing/Fixtures/RouterTestFixture.cs b/src/__Libraries/__Tests/StellaOps.Router.Testing/Fixtures/RouterTestFixture.cs new file mode 100644 index 000000000..58c21591c --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Testing/Fixtures/RouterTestFixture.cs @@ -0,0 +1,102 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace StellaOps.Router.Testing.Fixtures; + +/// +/// Base test fixture for Router tests providing common utilities. +/// Implements IAsyncLifetime for async setup/teardown. +/// +public abstract class RouterTestFixture : IAsyncLifetime +{ + /// + /// Gets a null logger factory for tests that don't need logging. + /// + protected ILoggerFactory LoggerFactory { get; } = NullLoggerFactory.Instance; + + /// + /// Gets a null logger for tests that don't need logging. + /// + protected ILogger GetLogger() => NullLogger.Instance; + + /// + /// Creates a cancellation token that times out after the specified duration. + /// + protected static CancellationToken CreateTimeoutToken(TimeSpan timeout) + { + var cts = new CancellationTokenSource(timeout); + return cts.Token; + } + + /// + /// Creates a cancellation token that times out after 5 seconds (default for tests). + /// + protected static CancellationToken CreateTestTimeoutToken() + { + return CreateTimeoutToken(TimeSpan.FromSeconds(5)); + } + + /// + /// Waits for a condition to be true with timeout. + /// + protected static async Task WaitForConditionAsync( + Func condition, + TimeSpan timeout, + TimeSpan? pollInterval = null) + { + var interval = pollInterval ?? TimeSpan.FromMilliseconds(50); + var deadline = DateTimeOffset.UtcNow + timeout; + + while (DateTimeOffset.UtcNow < deadline) + { + if (condition()) + return; + + await Task.Delay(interval); + } + + throw new TimeoutException($"Condition not met within {timeout}"); + } + + /// + /// Waits for an async condition to be true with timeout. + /// + protected static async Task WaitForConditionAsync( + Func> condition, + TimeSpan timeout, + TimeSpan? pollInterval = null) + { + var interval = pollInterval ?? TimeSpan.FromMilliseconds(50); + var deadline = DateTimeOffset.UtcNow + timeout; + + while (DateTimeOffset.UtcNow < deadline) + { + if (await condition()) + return; + + await Task.Delay(interval); + } + + throw new TimeoutException($"Condition not met within {timeout}"); + } + + /// + /// Override for async initialization. + /// + public virtual Task InitializeAsync() => Task.CompletedTask; + + /// + /// Override for async cleanup. + /// + public virtual Task DisposeAsync() => Task.CompletedTask; +} + +/// +/// Collection fixture for sharing state across tests in the same collection. +/// +public abstract class RouterCollectionFixture : IAsyncLifetime +{ + public virtual Task InitializeAsync() => Task.CompletedTask; + public virtual Task DisposeAsync() => Task.CompletedTask; +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Testing/Mocks/MockConnectionState.cs b/src/__Libraries/__Tests/StellaOps.Router.Testing/Mocks/MockConnectionState.cs new file mode 100644 index 000000000..cfef09666 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Testing/Mocks/MockConnectionState.cs @@ -0,0 +1,56 @@ +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Testing.Mocks; + +/// +/// A mock connection state for testing routing and connection management. +/// +public sealed class MockConnectionState +{ + public string ConnectionId { get; init; } = Guid.NewGuid().ToString("N"); + public string ServiceName { get; init; } = "test-service"; + public string Version { get; init; } = "1.0.0"; + public string Region { get; init; } = "test"; + public string InstanceId { get; init; } = "test-instance"; + public InstanceHealthStatus HealthStatus { get; set; } = InstanceHealthStatus.Healthy; + public DateTimeOffset ConnectedAtUtc { get; init; } = DateTimeOffset.UtcNow; + public DateTimeOffset LastHeartbeatUtc { get; set; } = DateTimeOffset.UtcNow; + public int InflightRequests { get; set; } + public int Weight { get; set; } = 100; + public List Endpoints { get; init; } = new(); + + /// + /// Creates a connection state for testing. + /// + public static MockConnectionState Create( + string? serviceName = null, + string? instanceId = null, + InstanceHealthStatus status = InstanceHealthStatus.Healthy) + { + return new MockConnectionState + { + ServiceName = serviceName ?? "test-service", + InstanceId = instanceId ?? $"instance-{Guid.NewGuid():N}", + HealthStatus = status + }; + } + + /// + /// Creates multiple connection states simulating a service cluster. + /// + public static List CreateCluster( + string serviceName, + int instanceCount, + InstanceHealthStatus status = InstanceHealthStatus.Healthy) + { + return Enumerable.Range(0, instanceCount) + .Select(i => new MockConnectionState + { + ServiceName = serviceName, + InstanceId = $"{serviceName}-{i}", + HealthStatus = status + }) + .ToList(); + } +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Testing/Mocks/RecordingLogger.cs b/src/__Libraries/__Tests/StellaOps.Router.Testing/Mocks/RecordingLogger.cs new file mode 100644 index 000000000..dbcbf1d6d --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Testing/Mocks/RecordingLogger.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace StellaOps.Router.Testing.Mocks; + +/// +/// A logger that records all log entries for assertions. +/// +public sealed class RecordingLogger : ILogger +{ + private readonly ConcurrentQueue _entries = new(); + + /// + /// Gets all recorded log entries. + /// + public IReadOnlyList Entries => _entries.ToList(); + + /// + /// Gets entries filtered by log level. + /// + public IEnumerable GetEntries(LogLevel level) => + _entries.Where(e => e.Level == level); + + /// + /// Gets all error entries. + /// + public IEnumerable Errors => GetEntries(LogLevel.Error); + + /// + /// Gets all warning entries. + /// + public IEnumerable Warnings => GetEntries(LogLevel.Warning); + + /// + /// Clears all recorded entries. + /// + public void Clear() + { + while (_entries.TryDequeue(out _)) { } + } + + public IDisposable? BeginScope(TState state) where TState : notnull => + NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + _entries.Enqueue(new LogEntry + { + Level = logLevel, + EventId = eventId, + Message = formatter(state, exception), + Exception = exception, + Timestamp = DateTimeOffset.UtcNow + }); + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + public void Dispose() { } + } +} + +/// +/// Represents a recorded log entry. +/// +public sealed record LogEntry +{ + public required LogLevel Level { get; init; } + public required EventId EventId { get; init; } + public required string Message { get; init; } + public Exception? Exception { get; init; } + public DateTimeOffset Timestamp { get; init; } +} + +/// +/// A logger factory that creates recording loggers. +/// +public sealed class RecordingLoggerFactory : ILoggerFactory +{ + private readonly ConcurrentDictionary _loggers = new(); + + public ILogger CreateLogger(string categoryName) => + (ILogger)_loggers.GetOrAdd(categoryName, _ => new RecordingLogger()); + + public ILogger CreateLogger() => + (ILogger)_loggers.GetOrAdd(typeof(T).FullName!, _ => new RecordingLogger()); + + public RecordingLogger? GetLogger() => + _loggers.TryGetValue(typeof(T).FullName!, out var logger) + ? logger as RecordingLogger + : null; + + public void AddProvider(ILoggerProvider provider) { } + + public void Dispose() { } +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Testing/StellaOps.Router.Testing.csproj b/src/__Libraries/__Tests/StellaOps.Router.Testing/StellaOps.Router.Testing.csproj new file mode 100644 index 000000000..697f39158 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Testing/StellaOps.Router.Testing.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + preview + enable + enable + true + false + StellaOps.Router.Testing + + + + + + + + + + + + + + diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryChannelTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryChannelTests.cs new file mode 100644 index 000000000..12c1d629e --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryChannelTests.cs @@ -0,0 +1,300 @@ +using System.Threading.Channels; +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Transport.InMemory.Tests; + +/// +/// Unit tests for . +/// +public sealed class InMemoryChannelTests +{ + private static InstanceDescriptor CreateTestInstance() + { + return new InstanceDescriptor + { + InstanceId = "inst-456", + ServiceName = "test-service", + Version = "1.0.0", + Region = "default" + }; + } + + private static ConnectionState CreateTestConnectionState(string connectionId) + { + return new ConnectionState + { + ConnectionId = connectionId, + Instance = CreateTestInstance(), + TransportType = TransportType.InMemory + }; + } + + #region Constructor Tests + + [Fact] + public void Constructor_SetsConnectionId() + { + // Arrange & Act + using var channel = new InMemoryChannel("conn-123"); + + // Assert + channel.ConnectionId.Should().Be("conn-123"); + } + + [Fact] + public void Constructor_CreatesUnboundedChannels_ByDefault() + { + // Arrange & Act + using var channel = new InMemoryChannel("conn-123"); + + // Assert - channels should be able to accept multiple items without blocking + channel.ToMicroservice.Should().NotBeNull(); + channel.ToGateway.Should().NotBeNull(); + } + + [Fact] + public void Constructor_CreatesBoundedChannels_WhenBufferSizeSpecified() + { + // Arrange & Act + using var channel = new InMemoryChannel("conn-123", bufferSize: 10); + + // Assert + channel.ToMicroservice.Should().NotBeNull(); + channel.ToGateway.Should().NotBeNull(); + } + + [Fact] + public void Constructor_CreatesLifetimeToken() + { + // Arrange & Act + using var channel = new InMemoryChannel("conn-123"); + + // Assert + channel.LifetimeToken.Should().NotBeNull(); + channel.LifetimeToken.IsCancellationRequested.Should().BeFalse(); + } + + [Fact] + public void Constructor_Instance_IsInitiallyNull() + { + // Arrange & Act + using var channel = new InMemoryChannel("conn-123"); + + // Assert + channel.Instance.Should().BeNull(); + } + + [Fact] + public void Constructor_State_IsInitiallyNull() + { + // Arrange & Act + using var channel = new InMemoryChannel("conn-123"); + + // Assert + channel.State.Should().BeNull(); + } + + #endregion + + #region Property Assignment Tests + + [Fact] + public void Instance_CanBeSet() + { + // Arrange + using var channel = new InMemoryChannel("conn-123"); + var instance = CreateTestInstance(); + + // Act + channel.Instance = instance; + + // Assert + channel.Instance.Should().BeSameAs(instance); + channel.Instance.ServiceName.Should().Be("test-service"); + } + + [Fact] + public void State_CanBeSet() + { + // Arrange + using var channel = new InMemoryChannel("conn-123"); + var state = CreateTestConnectionState("conn-123"); + + // Act + channel.State = state; + + // Assert + channel.State.Should().BeSameAs(state); + } + + #endregion + + #region Channel Communication Tests + + [Fact] + public async Task ToMicroservice_CanWriteAndRead() + { + // Arrange + using var channel = new InMemoryChannel("conn-123"); + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "corr-123", + Payload = new byte[] { 1, 2, 3 } + }; + + // Act + await channel.ToMicroservice.Writer.WriteAsync(frame); + var received = await channel.ToMicroservice.Reader.ReadAsync(); + + // Assert + received.Should().BeSameAs(frame); + } + + [Fact] + public async Task ToGateway_CanWriteAndRead() + { + // Arrange + using var channel = new InMemoryChannel("conn-123"); + var frame = new Frame + { + Type = FrameType.Response, + CorrelationId = "corr-123", + Payload = new byte[] { 4, 5, 6 } + }; + + // Act + await channel.ToGateway.Writer.WriteAsync(frame); + var received = await channel.ToGateway.Reader.ReadAsync(); + + // Assert + received.Should().BeSameAs(frame); + } + + [Fact] + public async Task Channel_MultipleFrames_DeliveredInOrder() + { + // Arrange + using var channel = new InMemoryChannel("conn-123"); + var frame1 = new Frame { Type = FrameType.Request, CorrelationId = "1", Payload = Array.Empty() }; + var frame2 = new Frame { Type = FrameType.Request, CorrelationId = "2", Payload = Array.Empty() }; + var frame3 = new Frame { Type = FrameType.Request, CorrelationId = "3", Payload = Array.Empty() }; + + // Act + await channel.ToMicroservice.Writer.WriteAsync(frame1); + await channel.ToMicroservice.Writer.WriteAsync(frame2); + await channel.ToMicroservice.Writer.WriteAsync(frame3); + + var received1 = await channel.ToMicroservice.Reader.ReadAsync(); + var received2 = await channel.ToMicroservice.Reader.ReadAsync(); + var received3 = await channel.ToMicroservice.Reader.ReadAsync(); + + // Assert - FIFO ordering + received1.CorrelationId.Should().Be("1"); + received2.CorrelationId.Should().Be("2"); + received3.CorrelationId.Should().Be("3"); + } + + #endregion + + #region Bounded Channel Tests + + [Fact] + public async Task BoundedChannel_AcceptsUpToBufferSize() + { + // Arrange + using var channel = new InMemoryChannel("conn-123", bufferSize: 3); + var frame = new Frame { Type = FrameType.Request, CorrelationId = "test", Payload = Array.Empty() }; + + // Act & Assert - should accept 3 without blocking + await channel.ToMicroservice.Writer.WriteAsync(frame); + await channel.ToMicroservice.Writer.WriteAsync(frame); + await channel.ToMicroservice.Writer.WriteAsync(frame); + + // Channel now at capacity + var tryWrite = channel.ToMicroservice.Writer.TryWrite(frame); + tryWrite.Should().BeFalse(); // Full, can't write synchronously + } + + #endregion + + #region Dispose Tests + + [Fact] + public void Dispose_CancelsLifetimeToken() + { + // Arrange + var channel = new InMemoryChannel("conn-123"); + + // Act + channel.Dispose(); + + // Assert + channel.LifetimeToken.IsCancellationRequested.Should().BeTrue(); + } + + [Fact] + public void Dispose_CompletesChannels() + { + // Arrange + var channel = new InMemoryChannel("conn-123"); + + // Act + channel.Dispose(); + + // Assert + channel.ToMicroservice.Reader.Completion.IsCompleted.Should().BeTrue(); + channel.ToGateway.Reader.Completion.IsCompleted.Should().BeTrue(); + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + // Arrange + var channel = new InMemoryChannel("conn-123"); + + // Act + var action = () => + { + channel.Dispose(); + channel.Dispose(); + channel.Dispose(); + }; + + // Assert + action.Should().NotThrow(); + } + + [Fact] + public async Task Dispose_ReaderDetectsCompletion() + { + // Arrange + using var channel = new InMemoryChannel("conn-123"); + + // Start reader task + var readerTask = Task.Run(async () => + { + var completed = false; + try + { + await channel.ToMicroservice.Reader.ReadAsync(); + } + catch (ChannelClosedException) + { + completed = true; + } + return completed; + }); + + // Act + await Task.Delay(50); // Give reader time to start waiting + channel.Dispose(); + + // Assert + var result = await readerTask; + result.Should().BeTrue(); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryConnectionRegistryTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryConnectionRegistryTests.cs new file mode 100644 index 000000000..c041e5a30 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryConnectionRegistryTests.cs @@ -0,0 +1,458 @@ +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Transport.InMemory.Tests; + +/// +/// Unit tests for . +/// +public sealed class InMemoryConnectionRegistryTests : IDisposable +{ + private readonly InMemoryConnectionRegistry _registry; + + public InMemoryConnectionRegistryTests() + { + _registry = new InMemoryConnectionRegistry(); + } + + public void Dispose() + { + _registry.Dispose(); + } + + private static InstanceDescriptor CreateTestInstance(string instanceId = "inst-1", string serviceName = "test-service", string version = "1.0") + { + return new InstanceDescriptor + { + InstanceId = instanceId, + ServiceName = serviceName, + Version = version, + Region = "default" + }; + } + + private static ConnectionState CreateTestConnectionState(string connectionId) + { + return new ConnectionState + { + ConnectionId = connectionId, + Instance = CreateTestInstance(), + TransportType = TransportType.InMemory + }; + } + + #region CreateChannel Tests + + [Fact] + public void CreateChannel_ReturnsNewChannel() + { + // Arrange & Act + var channel = _registry.CreateChannel("conn-123"); + + // Assert + channel.Should().NotBeNull(); + channel.ConnectionId.Should().Be("conn-123"); + } + + [Fact] + public void CreateChannel_IncreasesCount() + { + // Arrange + _registry.Count.Should().Be(0); + + // Act + _registry.CreateChannel("conn-123"); + + // Assert + _registry.Count.Should().Be(1); + } + + [Fact] + public void CreateChannel_WithBufferSize_CreatesCorrectChannel() + { + // Arrange & Act + var channel = _registry.CreateChannel("conn-123", bufferSize: 100); + + // Assert + channel.Should().NotBeNull(); + } + + [Fact] + public void CreateChannel_DuplicateId_ThrowsInvalidOperationException() + { + // Arrange + _registry.CreateChannel("conn-123"); + + // Act + var action = () => _registry.CreateChannel("conn-123"); + + // Assert + action.Should().Throw() + .WithMessage("*conn-123*already exists*"); + } + + [Fact] + public void CreateChannel_AfterDispose_ThrowsObjectDisposedException() + { + // Arrange + _registry.Dispose(); + + // Act + var action = () => _registry.CreateChannel("conn-123"); + + // Assert + action.Should().Throw(); + } + + #endregion + + #region GetChannel Tests + + [Fact] + public void GetChannel_ExistingConnection_ReturnsChannel() + { + // Arrange + var created = _registry.CreateChannel("conn-123"); + + // Act + var retrieved = _registry.GetChannel("conn-123"); + + // Assert + retrieved.Should().BeSameAs(created); + } + + [Fact] + public void GetChannel_NonexistentConnection_ReturnsNull() + { + // Arrange & Act + var retrieved = _registry.GetChannel("nonexistent"); + + // Assert + retrieved.Should().BeNull(); + } + + #endregion + + #region GetRequiredChannel Tests + + [Fact] + public void GetRequiredChannel_ExistingConnection_ReturnsChannel() + { + // Arrange + var created = _registry.CreateChannel("conn-123"); + + // Act + var retrieved = _registry.GetRequiredChannel("conn-123"); + + // Assert + retrieved.Should().BeSameAs(created); + } + + [Fact] + public void GetRequiredChannel_NonexistentConnection_ThrowsInvalidOperationException() + { + // Arrange & Act + var action = () => _registry.GetRequiredChannel("nonexistent"); + + // Assert + action.Should().Throw() + .WithMessage("*nonexistent*not found*"); + } + + #endregion + + #region RemoveChannel Tests + + [Fact] + public void RemoveChannel_ExistingConnection_ReturnsTrue() + { + // Arrange + _registry.CreateChannel("conn-123"); + + // Act + var result = _registry.RemoveChannel("conn-123"); + + // Assert + result.Should().BeTrue(); + _registry.Count.Should().Be(0); + } + + [Fact] + public void RemoveChannel_NonexistentConnection_ReturnsFalse() + { + // Arrange & Act + var result = _registry.RemoveChannel("nonexistent"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void RemoveChannel_DisposesChannel() + { + // Arrange + var channel = _registry.CreateChannel("conn-123"); + var token = channel.LifetimeToken; + + // Act + _registry.RemoveChannel("conn-123"); + + // Assert + token.IsCancellationRequested.Should().BeTrue(); + } + + [Fact] + public void RemoveChannel_CannotGetAfterRemove() + { + // Arrange + _registry.CreateChannel("conn-123"); + + // Act + _registry.RemoveChannel("conn-123"); + var retrieved = _registry.GetChannel("conn-123"); + + // Assert + retrieved.Should().BeNull(); + } + + #endregion + + #region ConnectionIds Tests + + [Fact] + public void ConnectionIds_EmptyRegistry_ReturnsEmpty() + { + // Arrange & Act + var ids = _registry.ConnectionIds; + + // Assert + ids.Should().BeEmpty(); + } + + [Fact] + public void ConnectionIds_WithConnections_ReturnsAllIds() + { + // Arrange + _registry.CreateChannel("conn-1"); + _registry.CreateChannel("conn-2"); + _registry.CreateChannel("conn-3"); + + // Act + var ids = _registry.ConnectionIds.ToList(); + + // Assert + ids.Should().HaveCount(3); + ids.Should().Contain("conn-1"); + ids.Should().Contain("conn-2"); + ids.Should().Contain("conn-3"); + } + + #endregion + + #region Count Tests + + [Fact] + public void Count_EmptyRegistry_IsZero() + { + // Arrange & Act & Assert + _registry.Count.Should().Be(0); + } + + [Fact] + public void Count_ReflectsActiveConnections() + { + // Arrange & Act + _registry.CreateChannel("conn-1"); + _registry.CreateChannel("conn-2"); + _registry.Count.Should().Be(2); + + _registry.RemoveChannel("conn-1"); + _registry.Count.Should().Be(1); + } + + #endregion + + #region GetAllConnections Tests + + [Fact] + public void GetAllConnections_EmptyRegistry_ReturnsEmpty() + { + // Arrange & Act + var connections = _registry.GetAllConnections(); + + // Assert + connections.Should().BeEmpty(); + } + + [Fact] + public void GetAllConnections_ChannelsWithoutState_ReturnsEmpty() + { + // Arrange + _registry.CreateChannel("conn-1"); + _registry.CreateChannel("conn-2"); + + // Act + var connections = _registry.GetAllConnections(); + + // Assert + connections.Should().BeEmpty(); // No State set on channels + } + + [Fact] + public void GetAllConnections_ChannelsWithState_ReturnsStates() + { + // Arrange + var channel1 = _registry.CreateChannel("conn-1"); + channel1.State = CreateTestConnectionState("conn-1"); + + var channel2 = _registry.CreateChannel("conn-2"); + channel2.State = CreateTestConnectionState("conn-2"); + + // Act + var connections = _registry.GetAllConnections(); + + // Assert + connections.Should().HaveCount(2); + } + + #endregion + + #region GetConnectionsFor Tests + + [Fact] + public void GetConnectionsFor_NoMatchingConnections_ReturnsEmpty() + { + // Arrange + _registry.CreateChannel("conn-1"); + + // Act + var connections = _registry.GetConnectionsFor("test-service", "1.0", "GET", "/api/users"); + + // Assert + connections.Should().BeEmpty(); + } + + [Fact] + public void GetConnectionsFor_MatchingServiceAndEndpoint_ReturnsConnections() + { + // Arrange + var channel = _registry.CreateChannel("conn-1"); + channel.Instance = CreateTestInstance("inst-1", "test-service", "1.0"); + channel.State = CreateTestConnectionState("conn-1"); + channel.State.Endpoints[("GET", "/api/users")] = new EndpointDescriptor + { + ServiceName = "test-service", + Version = "1.0", + Method = "GET", + Path = "/api/users" + }; + + // Act + var connections = _registry.GetConnectionsFor("test-service", "1.0", "GET", "/api/users"); + + // Assert + connections.Should().HaveCount(1); + } + + [Fact] + public void GetConnectionsFor_MismatchedVersion_ReturnsEmpty() + { + // Arrange + var channel = _registry.CreateChannel("conn-1"); + channel.Instance = CreateTestInstance("inst-1", "test-service", "1.0"); + channel.State = CreateTestConnectionState("conn-1"); + channel.State.Endpoints[("GET", "/api/users")] = new EndpointDescriptor + { + ServiceName = "test-service", + Version = "1.0", + Method = "GET", + Path = "/api/users" + }; + + // Act + var connections = _registry.GetConnectionsFor("test-service", "2.0", "GET", "/api/users"); + + // Assert + connections.Should().BeEmpty(); + } + + #endregion + + #region Dispose Tests + + [Fact] + public void Dispose_DisposesAllChannels() + { + // Arrange + var channel1 = _registry.CreateChannel("conn-1"); + var channel2 = _registry.CreateChannel("conn-2"); + var token1 = channel1.LifetimeToken; + var token2 = channel2.LifetimeToken; + + // Act + _registry.Dispose(); + + // Assert + token1.IsCancellationRequested.Should().BeTrue(); + token2.IsCancellationRequested.Should().BeTrue(); + } + + [Fact] + public void Dispose_ClearsRegistry() + { + // Arrange + _registry.CreateChannel("conn-1"); + _registry.CreateChannel("conn-2"); + + // Act + _registry.Dispose(); + + // Assert - Count may not be accurate after dispose, but GetChannel should not work + // We need a separate test for post-dispose behavior + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + // Arrange + _registry.CreateChannel("conn-1"); + + // Act + var action = () => + { + _registry.Dispose(); + _registry.Dispose(); + _registry.Dispose(); + }; + + // Assert + action.Should().NotThrow(); + } + + #endregion + + #region Concurrency Tests + + [Fact] + public async Task ConcurrentOperations_ThreadSafe() + { + // Arrange + var tasks = new List(); + var connectionCount = 100; + + // Act - Create and remove channels concurrently + for (int i = 0; i < connectionCount; i++) + { + var id = $"conn-{i}"; + tasks.Add(Task.Run(() => _registry.CreateChannel(id))); + } + + await Task.WhenAll(tasks); + + // Assert + _registry.Count.Should().Be(connectionCount); + _registry.ConnectionIds.Should().HaveCount(connectionCount); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryTransportOptionsTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryTransportOptionsTests.cs new file mode 100644 index 000000000..e22ffc031 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/InMemoryTransportOptionsTests.cs @@ -0,0 +1,176 @@ +namespace StellaOps.Router.Transport.InMemory.Tests; + +/// +/// Unit tests for . +/// +public sealed class InMemoryTransportOptionsTests +{ + #region Default Values Tests + + [Fact] + public void Constructor_DefaultTimeout_Is30Seconds() + { + // Arrange & Act + var options = new InMemoryTransportOptions(); + + // Assert + options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); + } + + [Fact] + public void Constructor_SimulatedLatency_IsZero() + { + // Arrange & Act + var options = new InMemoryTransportOptions(); + + // Assert + options.SimulatedLatency.Should().Be(TimeSpan.Zero); + } + + [Fact] + public void Constructor_ChannelBufferSize_IsZero() + { + // Arrange & Act + var options = new InMemoryTransportOptions(); + + // Assert + options.ChannelBufferSize.Should().Be(0); + } + + [Fact] + public void Constructor_HeartbeatInterval_Is10Seconds() + { + // Arrange & Act + var options = new InMemoryTransportOptions(); + + // Assert + options.HeartbeatInterval.Should().Be(TimeSpan.FromSeconds(10)); + } + + [Fact] + public void Constructor_HeartbeatTimeout_Is30Seconds() + { + // Arrange & Act + var options = new InMemoryTransportOptions(); + + // Assert + options.HeartbeatTimeout.Should().Be(TimeSpan.FromSeconds(30)); + } + + #endregion + + #region Property Assignment Tests + + [Fact] + public void DefaultTimeout_CanBeSet() + { + // Arrange + var options = new InMemoryTransportOptions(); + + // Act + options.DefaultTimeout = TimeSpan.FromMinutes(5); + + // Assert + options.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5)); + } + + [Fact] + public void SimulatedLatency_CanBeSet() + { + // Arrange + var options = new InMemoryTransportOptions(); + + // Act + options.SimulatedLatency = TimeSpan.FromMilliseconds(100); + + // Assert + options.SimulatedLatency.Should().Be(TimeSpan.FromMilliseconds(100)); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void ChannelBufferSize_CanBeSet(int bufferSize) + { + // Arrange + var options = new InMemoryTransportOptions(); + + // Act + options.ChannelBufferSize = bufferSize; + + // Assert + options.ChannelBufferSize.Should().Be(bufferSize); + } + + [Fact] + public void HeartbeatInterval_CanBeSet() + { + // Arrange + var options = new InMemoryTransportOptions(); + + // Act + options.HeartbeatInterval = TimeSpan.FromSeconds(5); + + // Assert + options.HeartbeatInterval.Should().Be(TimeSpan.FromSeconds(5)); + } + + [Fact] + public void HeartbeatTimeout_CanBeSet() + { + // Arrange + var options = new InMemoryTransportOptions(); + + // Act + options.HeartbeatTimeout = TimeSpan.FromMinutes(1); + + // Assert + options.HeartbeatTimeout.Should().Be(TimeSpan.FromMinutes(1)); + } + + #endregion + + #region Typical Configuration Tests + + [Fact] + public void TypicalConfiguration_DevelopmentEnvironment() + { + // Arrange & Act + var options = new InMemoryTransportOptions + { + DefaultTimeout = TimeSpan.FromMinutes(5), // Longer timeout for debugging + SimulatedLatency = TimeSpan.Zero, // Instant for development + ChannelBufferSize = 0, // Unbounded + HeartbeatInterval = TimeSpan.FromSeconds(30), + HeartbeatTimeout = TimeSpan.FromMinutes(5) + }; + + // Assert + options.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(5)); + options.SimulatedLatency.Should().Be(TimeSpan.Zero); + options.ChannelBufferSize.Should().Be(0); + } + + [Fact] + public void TypicalConfiguration_TestingWithSimulatedLatency() + { + // Arrange & Act + var options = new InMemoryTransportOptions + { + DefaultTimeout = TimeSpan.FromSeconds(60), + SimulatedLatency = TimeSpan.FromMilliseconds(50), // Simulate network latency + ChannelBufferSize = 100, // Bounded for testing backpressure + HeartbeatInterval = TimeSpan.FromSeconds(5), + HeartbeatTimeout = TimeSpan.FromSeconds(15) + }; + + // Assert + options.SimulatedLatency.Should().Be(TimeSpan.FromMilliseconds(50)); + options.ChannelBufferSize.Should().Be(100); + options.HeartbeatTimeout.Should().BeGreaterThan(options.HeartbeatInterval); + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/StellaOps.Router.Transport.InMemory.Tests.csproj b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/StellaOps.Router.Transport.InMemory.Tests.csproj new file mode 100644 index 000000000..94ba71685 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.InMemory.Tests/StellaOps.Router.Transport.InMemory.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + preview + enable + enable + true + + $(NoWarn);CA2255 + false + StellaOps.Router.Transport.InMemory.Tests + + false + + + + + + + + + + + + + + + + + + diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/RabbitMqFrameProtocolTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/RabbitMqFrameProtocolTests.cs new file mode 100644 index 000000000..3122b7c0c --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/RabbitMqFrameProtocolTests.cs @@ -0,0 +1,436 @@ +using RabbitMQ.Client; +using StellaOps.Router.Common.Enums; +using StellaOps.Router.Common.Models; + +namespace StellaOps.Router.Transport.RabbitMq.Tests; + +/// +/// Unit tests for . +/// +public sealed class RabbitMqFrameProtocolTests +{ + #region ParseFrame Tests + + [Fact] + public void ParseFrame_WithValidProperties_ReturnsFrame() + { + // Arrange + var body = new byte[] { 1, 2, 3, 4, 5 }; + var properties = new StubBasicProperties + { + Type = "Request", + CorrelationId = "test-correlation-id" + }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Request); + frame.CorrelationId.Should().Be("test-correlation-id"); + frame.Payload.ToArray().Should().BeEquivalentTo(body); + } + + [Fact] + public void ParseFrame_WithResponseType_ReturnsResponseFrame() + { + // Arrange + var body = new byte[] { 1, 2 }; + var properties = new StubBasicProperties { Type = "Response", CorrelationId = "resp-123" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Response); + } + + [Fact] + public void ParseFrame_WithHelloType_ReturnsHelloFrame() + { + // Arrange + var body = Array.Empty(); + var properties = new StubBasicProperties { Type = "Hello", CorrelationId = "hello-123" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Hello); + } + + [Fact] + public void ParseFrame_WithHeartbeatType_ReturnsHeartbeatFrame() + { + // Arrange + var body = Array.Empty(); + var properties = new StubBasicProperties { Type = "Heartbeat", CorrelationId = "hb-123" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Heartbeat); + } + + [Fact] + public void ParseFrame_WithCancelType_ReturnsCancelFrame() + { + // Arrange + var body = Array.Empty(); + var properties = new StubBasicProperties { Type = "Cancel", CorrelationId = "cancel-123" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Cancel); + } + + [Fact] + public void ParseFrame_WithNullType_DefaultsToRequest() + { + // Arrange + var body = new byte[] { 1 }; + var properties = new StubBasicProperties { Type = null, CorrelationId = "test" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Request); + } + + [Fact] + public void ParseFrame_WithEmptyType_DefaultsToRequest() + { + // Arrange + var body = new byte[] { 1 }; + var properties = new StubBasicProperties { Type = "", CorrelationId = "test" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Request); + } + + [Fact] + public void ParseFrame_WithInvalidType_DefaultsToRequest() + { + // Arrange + var body = new byte[] { 1 }; + var properties = new StubBasicProperties { Type = "InvalidType", CorrelationId = "test" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Request); + } + + [Fact] + public void ParseFrame_CaseInsensitive_ParsesType() + { + // Arrange + var body = new byte[] { 1 }; + var properties = new StubBasicProperties { Type = "rEsPoNsE", CorrelationId = "test" }; + + // Act + var frame = RabbitMqFrameProtocol.ParseFrame(body, properties); + + // Assert + frame.Type.Should().Be(FrameType.Response); + } + + #endregion + + #region CreateProperties Tests + + [Fact] + public void CreateProperties_WithFrame_SetsTypeProperty() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Response, + CorrelationId = "test-123", + Payload = Array.Empty() + }; + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null); + + // Assert + properties.Type.Should().Be("Response"); + } + + [Fact] + public void CreateProperties_WithCorrelationId_SetsCorrelationId() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "my-correlation-id", + Payload = Array.Empty() + }; + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null); + + // Assert + properties.CorrelationId.Should().Be("my-correlation-id"); + } + + [Fact] + public void CreateProperties_WithReplyTo_SetsReplyTo() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, "my-reply-queue"); + + // Assert + properties.ReplyTo.Should().Be("my-reply-queue"); + } + + [Fact] + public void CreateProperties_WithNullReplyTo_DoesNotSetReplyTo() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null); + + // Assert + properties.ReplyTo.Should().BeNullOrEmpty(); + } + + [Fact] + public void CreateProperties_WithTimeout_SetsExpiration() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + var timeout = TimeSpan.FromSeconds(30); + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null, timeout); + + // Assert + properties.Expiration.Should().Be("30000"); + } + + [Fact] + public void CreateProperties_WithoutTimeout_DoesNotSetExpiration() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null, null); + + // Assert + properties.Expiration.Should().BeNullOrEmpty(); + } + + [Fact] + public void CreateProperties_SetsTimestamp() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + var beforeTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null); + var afterTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + // Assert + properties.Timestamp.UnixTime.Should().BeInRange(beforeTimestamp, afterTimestamp); + } + + [Fact] + public void CreateProperties_SetsTransientDeliveryMode() + { + // Arrange + var frame = new Frame + { + Type = FrameType.Request, + CorrelationId = "test", + Payload = Array.Empty() + }; + + // Act + var properties = RabbitMqFrameProtocol.CreateProperties(frame, null); + + // Assert + properties.DeliveryMode.Should().Be(DeliveryModes.Transient); + } + + #endregion + + #region ExtractConnectionId Tests + + [Fact] + public void ExtractConnectionId_WithReplyTo_ExtractsFromQueueName() + { + // Arrange + var properties = new StubBasicProperties + { + Type = "Hello", + CorrelationId = "test", + ReplyTo = "stella.svc.instance-123" + }; + + // Act + var connectionId = RabbitMqFrameProtocol.ExtractConnectionId(properties); + + // Assert + connectionId.Should().Be("rmq-instance-123"); + } + + [Fact] + public void ExtractConnectionId_WithSimpleReplyTo_PrefixesWithRmq() + { + // Arrange + var properties = new StubBasicProperties + { + Type = "Hello", + CorrelationId = "test", + ReplyTo = "simple-queue" + }; + + // Act + var connectionId = RabbitMqFrameProtocol.ExtractConnectionId(properties); + + // Assert + connectionId.Should().Be("rmq-simple-queue"); + } + + [Fact] + public void ExtractConnectionId_WithoutReplyTo_UsesCorrelationId() + { + // Arrange + var properties = new StubBasicProperties + { + Type = "Hello", + CorrelationId = "abcd1234567890efgh", + ReplyTo = null + }; + + // Act + var connectionId = RabbitMqFrameProtocol.ExtractConnectionId(properties); + + // Assert + connectionId.Should().StartWith("rmq-"); + connectionId.Should().Contain("abcd1234567890ef"); + } + + [Fact] + public void ExtractConnectionId_WithShortCorrelationId_UsesEntireId() + { + // Arrange + var properties = new StubBasicProperties + { + Type = "Hello", + CorrelationId = "short", + ReplyTo = null + }; + + // Act + var connectionId = RabbitMqFrameProtocol.ExtractConnectionId(properties); + + // Assert + connectionId.Should().Be("rmq-short"); + } + + [Fact] + public void ExtractConnectionId_WithNoIdentifiers_GeneratesGuid() + { + // Arrange + var properties = new StubBasicProperties + { + Type = "Hello", + CorrelationId = null, + ReplyTo = null + }; + + // Act + var connectionId = RabbitMqFrameProtocol.ExtractConnectionId(properties); + + // Assert + connectionId.Should().StartWith("rmq-"); + connectionId.Length.Should().Be(32); + } + + #endregion + + #region Stub Implementation + + /// + /// Stub implementation of IReadOnlyBasicProperties for testing. + /// + private sealed class StubBasicProperties : IReadOnlyBasicProperties + { + public string? AppId { get; init; } + public string? ClusterId { get; init; } + public string? ContentEncoding { get; init; } + public string? ContentType { get; init; } + public string? CorrelationId { get; init; } + public DeliveryModes DeliveryMode { get; init; } + public string? Expiration { get; init; } + public IDictionary? Headers { get; init; } + public string? MessageId { get; init; } + public bool Persistent { get; init; } + public byte Priority { get; init; } + public string? ReplyTo { get; init; } + public PublicationAddress? ReplyToAddress { get; init; } + public AmqpTimestamp Timestamp { get; init; } + public string? Type { get; init; } + public string? UserId { get; init; } + + public bool IsAppIdPresent() => AppId != null; + public bool IsClusterIdPresent() => ClusterId != null; + public bool IsContentEncodingPresent() => ContentEncoding != null; + public bool IsContentTypePresent() => ContentType != null; + public bool IsCorrelationIdPresent() => CorrelationId != null; + public bool IsDeliveryModePresent() => true; + public bool IsExpirationPresent() => Expiration != null; + public bool IsHeadersPresent() => Headers != null; + public bool IsMessageIdPresent() => MessageId != null; + public bool IsPriorityPresent() => true; + public bool IsReplyToPresent() => ReplyTo != null; + public bool IsTimestampPresent() => true; + public bool IsTypePresent() => Type != null; + public bool IsUserIdPresent() => UserId != null; + } + + #endregion +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/RabbitMqTransportOptionsTests.cs b/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/RabbitMqTransportOptionsTests.cs new file mode 100644 index 000000000..6b054a192 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/RabbitMqTransportOptionsTests.cs @@ -0,0 +1,248 @@ +namespace StellaOps.Router.Transport.RabbitMq.Tests; + +/// +/// Unit tests for . +/// +public sealed class RabbitMqTransportOptionsTests +{ + [Fact] + public void DefaultOptions_HostName_IsLocalhost() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.HostName.Should().Be("localhost"); + } + + [Fact] + public void DefaultOptions_Port_Is5672() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.Port.Should().Be(5672); + } + + [Fact] + public void DefaultOptions_VirtualHost_IsRoot() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.VirtualHost.Should().Be("/"); + } + + [Fact] + public void DefaultOptions_UserName_IsGuest() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.UserName.Should().Be("guest"); + } + + [Fact] + public void DefaultOptions_Password_IsGuest() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.Password.Should().Be("guest"); + } + + [Fact] + public void DefaultOptions_UseSsl_IsFalse() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.UseSsl.Should().BeFalse(); + } + + [Fact] + public void DefaultOptions_SslCertPath_IsNull() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.SslCertPath.Should().BeNull(); + } + + [Fact] + public void DefaultOptions_DurableQueues_IsFalse() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.DurableQueues.Should().BeFalse(); + } + + [Fact] + public void DefaultOptions_AutoDeleteQueues_IsTrue() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.AutoDeleteQueues.Should().BeTrue(); + } + + [Fact] + public void DefaultOptions_PrefetchCount_Is10() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.PrefetchCount.Should().Be(10); + } + + [Fact] + public void DefaultOptions_ExchangePrefix_IsStellaRouter() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.ExchangePrefix.Should().Be("stella.router"); + } + + [Fact] + public void DefaultOptions_QueuePrefix_IsStella() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.QueuePrefix.Should().Be("stella"); + } + + [Fact] + public void RequestExchange_UsesExchangePrefix() + { + // Arrange + var options = new RabbitMqTransportOptions + { + ExchangePrefix = "custom.prefix" + }; + + // Act & Assert + options.RequestExchange.Should().Be("custom.prefix.requests"); + } + + [Fact] + public void ResponseExchange_UsesExchangePrefix() + { + // Arrange + var options = new RabbitMqTransportOptions + { + ExchangePrefix = "custom.prefix" + }; + + // Act & Assert + options.ResponseExchange.Should().Be("custom.prefix.responses"); + } + + [Fact] + public void DefaultOptions_NodeId_IsNull() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.NodeId.Should().BeNull(); + } + + [Fact] + public void DefaultOptions_InstanceId_IsNull() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.InstanceId.Should().BeNull(); + } + + [Fact] + public void DefaultOptions_AutomaticRecoveryEnabled_IsTrue() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.AutomaticRecoveryEnabled.Should().BeTrue(); + } + + [Fact] + public void DefaultOptions_NetworkRecoveryInterval_Is5Seconds() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.NetworkRecoveryInterval.Should().Be(TimeSpan.FromSeconds(5)); + } + + [Fact] + public void DefaultOptions_DefaultTimeout_Is30Seconds() + { + // Arrange & Act + var options = new RabbitMqTransportOptions(); + + // Assert + options.DefaultTimeout.Should().Be(TimeSpan.FromSeconds(30)); + } + + [Fact] + public void Options_CanBeCustomized() + { + // Arrange & Act + var options = new RabbitMqTransportOptions + { + HostName = "rabbitmq.example.com", + Port = 5673, + VirtualHost = "/vhost", + UserName = "admin", + Password = "secret", + UseSsl = true, + SslCertPath = "/path/to/cert.pem", + DurableQueues = true, + AutoDeleteQueues = false, + PrefetchCount = 50, + ExchangePrefix = "myapp", + QueuePrefix = "myqueues", + NodeId = "node-1", + InstanceId = "instance-1", + AutomaticRecoveryEnabled = false, + NetworkRecoveryInterval = TimeSpan.FromSeconds(10), + DefaultTimeout = TimeSpan.FromMinutes(1) + }; + + // Assert + options.HostName.Should().Be("rabbitmq.example.com"); + options.Port.Should().Be(5673); + options.VirtualHost.Should().Be("/vhost"); + options.UserName.Should().Be("admin"); + options.Password.Should().Be("secret"); + options.UseSsl.Should().BeTrue(); + options.SslCertPath.Should().Be("/path/to/cert.pem"); + options.DurableQueues.Should().BeTrue(); + options.AutoDeleteQueues.Should().BeFalse(); + options.PrefetchCount.Should().Be(50); + options.ExchangePrefix.Should().Be("myapp"); + options.QueuePrefix.Should().Be("myqueues"); + options.NodeId.Should().Be("node-1"); + options.InstanceId.Should().Be("instance-1"); + options.AutomaticRecoveryEnabled.Should().BeFalse(); + options.NetworkRecoveryInterval.Should().Be(TimeSpan.FromSeconds(10)); + options.DefaultTimeout.Should().Be(TimeSpan.FromMinutes(1)); + } +} diff --git a/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/StellaOps.Router.Transport.RabbitMq.Tests.csproj b/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/StellaOps.Router.Transport.RabbitMq.Tests.csproj new file mode 100644 index 000000000..37094a974 --- /dev/null +++ b/src/__Libraries/__Tests/StellaOps.Router.Transport.RabbitMq.Tests/StellaOps.Router.Transport.RabbitMq.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + preview + enable + enable + true + + $(NoWarn);CA2255 + false + StellaOps.Router.Transport.RabbitMq.Tests + + false + + + + + + + + + + + + + + + + + +