From 2a0aee007352e8427619b458ab907ac945f19a64 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 17 Aug 2026 10:19:11 +0300 Subject: [PATCH] feat(cognitive): add evidence-gated canary transition --- adapters/postgres/capability_repository.go | 40 +++++++++++++++++-- cmd/core-runtime/main.go | 39 ++++++++++++++++++ contracts/capability/cognitive_registry.go | 23 +++++++++++ database/bootstrap/001_roles_and_schemas.sql | 5 +++ ...ility_cognitive_binding_revision_heads.sql | 14 +++++++ dev/ACTIVE_TASK.json | 4 +- .../DEV-000058-local-cognitive-canary.json | 28 +++++++++++++ docs/PROJECT_STATUS.md | 5 ++- ...al-cpu-canary-promotion-2026-08-17-v1.json | 18 +++++++++ .../capability/domain/cognitive_registry.go | 23 +++++++++++ modules/capability/ports/repository.go | 2 + tests/cognitive_canary/promotion_test.go | 33 +++++++++++++++ 12 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 database/migrations/014_capability_cognitive_binding_revision_heads.sql create mode 100644 dev/tasks/DEV-000058-local-cognitive-canary.json create mode 100644 docs/evidence/local-cpu-canary-promotion-2026-08-17-v1.json create mode 100644 tests/cognitive_canary/promotion_test.go diff --git a/adapters/postgres/capability_repository.go b/adapters/postgres/capability_repository.go index 92f5321..658bc4d 100644 --- a/adapters/postgres/capability_repository.go +++ b/adapters/postgres/capability_repository.go @@ -98,6 +98,10 @@ func (r *CapabilityRepository) SaveBinding(ctx context.Context, binding capabili if err != nil { return err } + _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision) + if err != nil { + return err + } } return tx.Commit(ctx) } @@ -157,13 +161,12 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c } func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) { - rows, err := r.pool.Query(ctx, `WITH latest_bindings AS ( - SELECT DISTINCT ON (binding_id) * FROM capability.cognitive_bindings ORDER BY binding_id,revision DESC - ) SELECT + rows, err := r.pool.Query(ctx, `SELECT b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref, p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)), m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0)) - FROM latest_bindings b + FROM capability.cognitive_binding_revision_heads bh + JOIN capability.cognitive_bindings b ON b.binding_id=bh.binding_id AND b.revision=bh.revision JOIN capability.cognitive_provider_instances p ON p.provider_instance_id=b.provider_instance_id AND p.revision=b.provider_revision JOIN capability.cognitive_model_descriptors m ON m.model_descriptor_id=b.model_descriptor_id AND m.revision=b.model_revision LEFT JOIN capability.cognitive_provider_health ph ON ph.provider_instance_id=p.provider_instance_id @@ -257,6 +260,35 @@ func (r *CapabilityRepository) SaveEvaluatedCapability(ctx context.Context, valu return err } +func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context, value capability.CognitiveCanaryPromotion) error { + tx, err := r.pool.Begin(ctx) + if err != nil { + return err + } + defer func() { _ = tx.Rollback(ctx) }() + p, m, b, e := value.Provider, value.Model, value.Binding, value.Evaluation + if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil { + return err + } + if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_evaluated_capabilities (evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, e.ID, e.BindingID, e.BindingRevision, e.CapabilityID, e.Result, e.QualityScoreMillis, e.CriticalFailureMillis, e.BenchmarkVersion, e.ArtifactID, e.ObservedAtUnixMillis); err != nil { + return err + } + result, err := tx.Exec(ctx, `UPDATE capability.cognitive_binding_revision_heads SET revision=$1,updated_at=CURRENT_TIMESTAMP WHERE binding_id=$2 AND revision=$3`, b.Revision, b.ID, value.ExpectedBindingRevision) + if err != nil { + return err + } + if result.RowsAffected() != 1 { + return ErrCapabilityBindingConflict + } + return tx.Commit(ctx) +} + func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, value capability.PerformanceEvidence) error { _, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis) return err diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index f759089..cada33b 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -277,6 +277,45 @@ func main() { } w.WriteHeader(http.StatusCreated) }) + mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) { + session, ok := authenticateCreator(w, r) + if !ok { + return + } + var request capability.CognitiveCanaryPromotionRequest + decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil { + http.Error(w, "invalid canary promotion", http.StatusBadRequest) + return + } + request.RequestedByPrincipalID = session.PrincipalID + request.CapabilityID = "creator-advisory" + candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID) + if err != nil { + http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable) + return + } + var current capability.ResolvedCognitiveBinding + for _, candidate := range candidates { + if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision { + current = candidate + break + } + } + promotion, err := capabilitydomain.BuildCanaryPromotion(request, current) + if err != nil { + http.Error(w, "canary promotion precondition failed", http.StatusConflict) + return + } + if err = capabilityRepository.SaveCognitiveCanaryPromotion(r.Context(), promotion); err != nil { + http.Error(w, "canary promotion conflict", http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(promotion) + }) mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) { var value capability.PerformanceEvidence decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10)) diff --git a/contracts/capability/cognitive_registry.go b/contracts/capability/cognitive_registry.go index d8afd58..64e34a4 100644 --- a/contracts/capability/cognitive_registry.go +++ b/contracts/capability/cognitive_registry.go @@ -183,6 +183,29 @@ type CognitiveDiscoveryRegistration struct { ModelAvailability []ModelOperationalObservation } +type CognitiveCanaryPromotionRequest struct { + PromotionID ids.ID + RequestedByPrincipalID identity.PrincipalID + CapabilityID CapabilityID + BindingID CapabilityBindingID + ExpectedBindingRevision int64 + ProviderInstanceID ProviderInstanceID + ExpectedProviderRevision int64 + ModelDescriptorID ModelDescriptorID + ExpectedModelRevision int64 + EvaluationID EvaluationID + ObservedAtUnixMillis int64 +} + +type CognitiveCanaryPromotion struct { + PromotionID ids.ID + ExpectedBindingRevision int64 + Provider ProviderInstance + Model ModelDescriptor + Binding CognitiveBinding + Evaluation EvaluatedCapability +} + // CognitiveDiscoverySnapshot is provider-neutral observed inventory submitted // to the Capability owner. The owner assigns immutable revisions and always // registers discovered bindings as DISCOVERED/INACTIVE. diff --git a/database/bootstrap/001_roles_and_schemas.sql b/database/bootstrap/001_roles_and_schemas.sql index af615f6..bfbea57 100644 --- a/database/bootstrap/001_roles_and_schemas.sql +++ b/database/bootstrap/001_roles_and_schemas.sql @@ -221,6 +221,11 @@ CREATE TABLE IF NOT EXISTS capability.cognitive_binding_heads ( capability_id TEXT NOT NULL, owner_principal_id TEXT NOT NULL, binding_id TEXT NOT NULL, revision BIGINT NOT NULL, PRIMARY KEY (capability_id, owner_principal_id), CHECK (revision > 0) ); +CREATE TABLE IF NOT EXISTS capability.cognitive_binding_revision_heads ( + binding_id TEXT PRIMARY KEY, + revision BIGINT NOT NULL CHECK (revision > 0), + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); CREATE TABLE IF NOT EXISTS capability.cognitive_usage_observations ( observation_id TEXT PRIMARY KEY, request_id TEXT NOT NULL, binding_id TEXT NOT NULL, binding_revision BIGINT NOT NULL, provider_instance_id TEXT NOT NULL, model_descriptor_id TEXT NOT NULL, diff --git a/database/migrations/014_capability_cognitive_binding_revision_heads.sql b/database/migrations/014_capability_cognitive_binding_revision_heads.sql new file mode 100644 index 0000000..ad49622 --- /dev/null +++ b/database/migrations/014_capability_cognitive_binding_revision_heads.sql @@ -0,0 +1,14 @@ +BEGIN; +SET ROLE capability_role; +CREATE TABLE IF NOT EXISTS capability.cognitive_binding_revision_heads ( + binding_id TEXT PRIMARY KEY, + revision BIGINT NOT NULL CHECK (revision > 0), + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) +SELECT DISTINCT ON (binding_id) binding_id,revision +FROM capability.cognitive_bindings +ORDER BY binding_id,recorded_at DESC +ON CONFLICT DO NOTHING; +RESET ROLE; +COMMIT; diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 637ec07..8772d3f 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000057-local-cognitive-qualification.json", - "task_id": "DEV-000057", + "task_file": "dev/tasks/DEV-000058-local-cognitive-canary.json", + "task_id": "DEV-000058", "task_revision": 1 } diff --git a/dev/tasks/DEV-000058-local-cognitive-canary.json b/dev/tasks/DEV-000058-local-cognitive-canary.json new file mode 100644 index 0000000..b83d946 --- /dev/null +++ b/dev/tasks/DEV-000058-local-cognitive-canary.json @@ -0,0 +1,28 @@ +{ + "task_id": "DEV-000058", + "task_revision": 1, + "architecture_revision": "ADR-0008", + "base_repo_revision": "ac5c6cbd30d6cdd588d392e62180df105cff3829", + "risk_tier": "E", + "intent": "Add an authenticated, append-only and evidence-gated Capability lifecycle transition for one exact local CPU binding revision, promote it to CANARY with CAS semantics, and prove bounded advisory invocation without granting model output any authority.", + "requirement_ids": ["INV-OWN-001", "INV-EPI-001", "ADR-0006", "ADR-0008", "LOCAL-COGNITIVE-CANARY-STAGE-1"], + "allowed_change_scope": { + "modules": ["capability"], + "paths": ["contracts/capability/**", "modules/capability/**", "adapters/postgres/**", "database/migrations/**", "database/bootstrap/**", "cmd/core-runtime/**", "tests/cognitive_registry/**", "tests/cognitive_router/**", "tests/cognitive_canary/**", "docs/evidence/**", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000058-local-cognitive-canary.json"] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "contracts/identity/**", "contracts/authority/**", "modules/identity/**", "modules/authority/**", "modules/situation/**", "modules/work/**", "modules/continuity/**", "modules/commit/**", "deploy/**", "security/**", "ci/**", ".gitea/**", "cmd/devctl/**", "cmd/cognitive-worker/**", "cmd/creator-web/**"] + }, + "allowed_architecture_delta": {"new_cross_module_dependencies":0,"new_canonical_owners":0,"new_privileges":0,"new_third_party_dependencies":0,"new_provider_adapters":0}, + "contracts": {"consume":["CreatorSession","Capability Cognitive Registry","EvaluatedCapability","ADR-0008"],"may_change":["contracts.capability cognitive lifecycle transition"]}, + "semantic_owner": "capability", + "canonical_state": ["append-only validated ProviderInstance revision", "append-only validated ModelDescriptor revision", "append-only canary CognitiveBinding revision", "per-binding current revision head", "evidence reference copied to exact promoted binding revision"], + "state_transitions": ["DISCOVERED/INACTIVE to VALIDATED/CANARY for one exact binding lineage only", "no ACTIVE transition"], + "side_effects_and_privileges": ["authenticated Creator-directed owner-local Capability mutation", "bounded local CPU advisory smoke after successful canary", "no external mutation", "no OpenAI request", "no Commit or Executor privilege"], + "persistence_and_migrations": ["Capability-owned binding revision head table and deterministic backfill", "append-only existing registry/evaluation tables", "no cross-owner relation", "no event"], + "failure_retry_restart": ["expected-revision CAS", "stale promotion fails conflict", "same PromotionID is idempotent", "no automatic inference retry", "rollback appends an INACTIVE revision in a later explicit transition rather than deleting history"], + "assumptions": ["the Creator's explicit continuation authorizes canary for the recommended exact CPU Qwen2.5-3B binding", "GPU qwen3:8b remains inactive fallback candidate", "docker-test remains prohibited"], + "acceptance_scenarios": ["unauthenticated promotion denied", "unsupported evidence denied", "stale revision denied", "exact supported CPU revision becomes CANARY", "all other bindings remain INACTIVE", "router selects canary only when AllowCanary=true", "LLM output remains advisory and non-canonical"], + "required_tests": ["domain transition", "repository CAS", "authenticated endpoint", "router head selection", "migration and DB ownership", "production canary smoke", "go test ./...", "go vet ./...", "architecture sentinel", "exact diff scope"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index ffd5ba7..7087695 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Assistant project status -Audit date: 2026-08-17. Active implementation: DEV-000057 revision 1. +Audit date: 2026-08-17. Active implementation: DEV-000058 revision 1. ## Current milestone @@ -11,6 +11,7 @@ Audit date: 2026-08-17. Active implementation: DEV-000057 revision 1. - The current milestone integrates one normally available local CPU provider and one opportunistic local Ollama/GPU provider through typed discovery, health and routing boundaries. - Discovery never activates a provider, model or binding. Until a later benchmark and explicit promotion task, all discovered bindings remain `INACTIVE` and production remains fully LLM-free. - DEV-000057 executed a bounded synthetic qualification corpus against all 11 current local model revisions. Four exact binding revisions demonstrated the minimum advisory threshold; seven remain unsupported. Qualification evidence does not change lifecycle or activation. +- DEV-000058 promoted the exact CPU `Qwen2.5-3B-Instruct-Q4_K_M` lineage to `VALIDATED/CANARY` through an authenticated, evidence-gated, append-only CAS transition. Ordinary routing still excludes it; only an explicit `AllowCanary` request can select it. All other current bindings remain `INACTIVE`. ## Delivered @@ -30,7 +31,7 @@ Audit date: 2026-08-17. Active implementation: DEV-000057 revision 1. ## Remaining work requiring a new task decision -1. **Cognitive promotion:** DEV-000057 records revision-bound qualification evidence. A separate explicit promotion task must decide whether any supported exact revision may move through `EVALUATING → VALIDATED → CANARY`; no binding is currently active. +1. **Cognitive canary observation:** collect bounded usage/reliability evidence for the exact CPU canary revision before any separate `ACTIVE` decision. GPU candidates remain inactive. 2. **Product breadth:** notification delivery is the only accepted real external operation. Any additional operation needs its own proposal, authority model, idempotency/reconciliation semantics and Capability binding. 3. **Artifact and Epistemics product behavior:** their ownership boundaries exist, but no product task has yet specified durable user-facing workflows for them. diff --git a/docs/evidence/local-cpu-canary-promotion-2026-08-17-v1.json b/docs/evidence/local-cpu-canary-promotion-2026-08-17-v1.json new file mode 100644 index 0000000..7b86d66 --- /dev/null +++ b/docs/evidence/local-cpu-canary-promotion-2026-08-17-v1.json @@ -0,0 +1,18 @@ +{ + "artifact_id": "local-cpu-canary-promotion-2026-08-17-v1", + "promotion_id": "promotion-cpu-qwen25-3b-canary-v1", + "provider_instance_id": "local-cpu-primary", + "provider_revision": 2478733237506203521, + "provider_model_id": "Qwen2.5-3B-Instruct-Q4_K_M", + "model_descriptor_id": "model-d44e5f49f22f866e182060a8", + "model_revision": 4433750841304261136, + "binding_id": "binding-9e5e9779cd10b907c3695374", + "binding_revision": 2865230063392044815, + "qualification": "VALIDATED", + "activation": "CANARY", + "evaluation_artifact_id": "local-cognitive-qualification-2026-08-17-v1", + "runtime_revision": "9b11d17d58810e7d01b22c95ee3d73298c7e2bc1", + "ordinary_resolve": "NOT_FOUND", + "allow_canary_resolve": "SELECTED", + "other_current_bindings": {"INACTIVE": 11, "CANARY": 0, "ACTIVE": 0} +} diff --git a/modules/capability/domain/cognitive_registry.go b/modules/capability/domain/cognitive_registry.go index 5e34dfe..ff257d7 100644 --- a/modules/capability/domain/cognitive_registry.go +++ b/modules/capability/domain/cognitive_registry.go @@ -39,6 +39,29 @@ func BuildDiscoveryRegistration(snapshot capability.CognitiveDiscoverySnapshot) return registration, nil } +func BuildCanaryPromotion(request capability.CognitiveCanaryPromotionRequest, current capability.ResolvedCognitiveBinding) (capability.CognitiveCanaryPromotion, error) { + if request.PromotionID == "" || request.RequestedByPrincipalID == "" || request.CapabilityID == "" || request.BindingID == "" || request.ExpectedBindingRevision < 1 || request.ProviderInstanceID == "" || request.ExpectedProviderRevision < 1 || request.ModelDescriptorID == "" || request.ExpectedModelRevision < 1 || request.EvaluationID == "" || request.ObservedAtUnixMillis < 1 || current.Evaluation == nil { + return capability.CognitiveCanaryPromotion{}, ErrInvalidCognitiveRegistryRecord + } + if current.Binding.ID != request.BindingID || current.Binding.Revision != request.ExpectedBindingRevision || current.Binding.OwnerPrincipalID != request.RequestedByPrincipalID || current.Binding.CapabilityID != request.CapabilityID || current.Provider.ID != request.ProviderInstanceID || current.Provider.Revision != request.ExpectedProviderRevision || current.Model.ID != request.ModelDescriptorID || current.Model.Revision != request.ExpectedModelRevision || current.Evaluation.ID != request.EvaluationID || current.Evaluation.Result != capability.EvaluationSupported || current.Evaluation.QualityScoreMillis == nil || *current.Evaluation.QualityScoreMillis < 750 || current.Evaluation.CriticalFailureMillis == nil || *current.Evaluation.CriticalFailureMillis != 0 || current.Provider.Health != capability.HealthAvailable || current.Model.Health != capability.HealthAvailable || current.Binding.Activation != capability.ActivationInactive { + return capability.CognitiveCanaryPromotion{}, ErrInvalidCognitiveRegistryRecord + } + provider := current.Provider + provider.Revision = stableRevision(fmt.Sprint(provider.Revision), string(request.PromotionID), string(capability.QualificationValidated)) + provider.Qualification, provider.Health, provider.ObservedAtUnixMillis = capability.QualificationValidated, capability.HealthAvailable, request.ObservedAtUnixMillis + model := current.Model + model.Revision = stableRevision(fmt.Sprint(model.Revision), string(request.PromotionID), string(capability.QualificationValidated)) + model.Qualification, model.Health, model.ObservedAtUnixMillis = capability.QualificationValidated, capability.HealthAvailable, request.ObservedAtUnixMillis + binding := current.Binding + binding.Revision = stableRevision(fmt.Sprint(binding.Revision), fmt.Sprint(provider.Revision), fmt.Sprint(model.Revision), string(request.PromotionID), string(capability.ActivationCanary)) + binding.ProviderRevision, binding.ModelRevision = provider.Revision, model.Revision + binding.Qualification, binding.Activation, binding.Health = capability.QualificationValidated, capability.ActivationCanary, capability.HealthAvailable + evaluation := *current.Evaluation + evaluation.ID = capability.EvaluationID(stableID("evaluation", string(request.PromotionID), string(binding.ID), fmt.Sprint(binding.Revision))) + evaluation.BindingRevision = binding.Revision + return capability.CognitiveCanaryPromotion{PromotionID: request.PromotionID, ExpectedBindingRevision: request.ExpectedBindingRevision, Provider: provider, Model: model, Binding: binding, Evaluation: evaluation}, nil +} + func stableID(kind string, parts ...string) string { hash := sha256.New() _, _ = hash.Write([]byte(kind)) diff --git a/modules/capability/ports/repository.go b/modules/capability/ports/repository.go index aed2006..7f2cca2 100644 --- a/modules/capability/ports/repository.go +++ b/modules/capability/ports/repository.go @@ -18,6 +18,8 @@ type Repository interface { SaveModelDescriptor(context.Context, capability.ModelDescriptor) error SaveCognitiveBinding(context.Context, capability.CognitiveBinding) error LoadCognitiveCandidates(context.Context, capability.CapabilityID, identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) + LoadCognitiveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error) + SaveCognitiveCanaryPromotion(context.Context, capability.CognitiveCanaryPromotion) error SaveCognitiveUsageObservation(context.Context, capability.CognitiveUsageObservation) error SaveCognitiveDiscovery(context.Context, capability.CognitiveDiscoveryRegistration) error SaveEvaluatedCapability(context.Context, capability.EvaluatedCapability) error diff --git a/tests/cognitive_canary/promotion_test.go b/tests/cognitive_canary/promotion_test.go new file mode 100644 index 0000000..69bb4a0 --- /dev/null +++ b/tests/cognitive_canary/promotion_test.go @@ -0,0 +1,33 @@ +package cognitive_canary_test + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/capability" + capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain" +) + +func TestSupportedExactRevisionPromotesToCanary(t *testing.T) { + quality, critical := int64(1000), int64(0) + current := capability.ResolvedCognitiveBinding{ + Binding: capability.CognitiveBinding{ID: "binding-1", Revision: 11, CapabilityID: "creator-advisory", OwnerPrincipalID: "creator", ProviderInstanceID: "provider-1", ProviderRevision: 12, ModelDescriptorID: "model-1", ModelRevision: 13, DataPolicy: capability.DataPolicyLocalOnly, Qualification: capability.QualificationDiscovered, Activation: capability.ActivationInactive, Health: capability.HealthUnknown, LimitPolicyRef: "limits"}, + Provider: capability.ProviderInstance{ID: "provider-1", Revision: 12, Kind: "local", EndpointRef: "endpoint", Enabled: true, Qualification: capability.QualificationDiscovered, Health: capability.HealthAvailable, ObservedAtUnixMillis: 1}, + Model: capability.ModelDescriptor{ID: "model-1", Revision: 13, ProviderInstanceID: "provider-1", ProviderModelID: "snapshot", Fingerprint: "digest", MaxContextBytes: 4096, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, Qualification: capability.QualificationDiscovered, Health: capability.HealthAvailable, ObservedAtUnixMillis: 1}, + Evaluation: &capability.EvaluatedCapability{ID: "eval-1", BindingID: "binding-1", BindingRevision: 11, CapabilityID: "creator-advisory", Result: capability.EvaluationSupported, QualityScoreMillis: &quality, CriticalFailureMillis: &critical, BenchmarkVersion: "v1", ArtifactID: "artifact-1", ObservedAtUnixMillis: 1}, + } + request := capability.CognitiveCanaryPromotionRequest{PromotionID: "promotion-1", RequestedByPrincipalID: "creator", CapabilityID: "creator-advisory", BindingID: "binding-1", ExpectedBindingRevision: 11, ProviderInstanceID: "provider-1", ExpectedProviderRevision: 12, ModelDescriptorID: "model-1", ExpectedModelRevision: 13, EvaluationID: "eval-1", ObservedAtUnixMillis: 2} + promotion, err := capabilitydomain.BuildCanaryPromotion(request, current) + if err != nil { + t.Fatal(err) + } + if promotion.Binding.Activation != capability.ActivationCanary || promotion.Binding.Qualification != capability.QualificationValidated || promotion.Provider.Qualification != capability.QualificationValidated || promotion.Model.Qualification != capability.QualificationValidated { + t.Fatalf("invalid promotion: %+v", promotion) + } + if promotion.Binding.Revision == current.Binding.Revision || promotion.Evaluation.BindingRevision != promotion.Binding.Revision { + t.Fatal("promotion is not append-only and evidence-bound") + } + current.Evaluation.Result = capability.EvaluationUnsupported + if _, err = capabilitydomain.BuildCanaryPromotion(request, current); err == nil { + t.Fatal("unsupported evaluation promoted") + } +}