From cb92f955508540bedd059963e511b26df3ed6fbf Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 11:57:43 +0300 Subject: [PATCH] Add cognitive admission policy --- contracts/capability/admission.go | 68 +++++++++++++++++++ dev/ACTIVE_TASK.json | 4 +- ...EV-000115-cognitive-admission-control.json | 23 +++++++ tests/cognitive_admission/admission_test.go | 37 ++++++++++ 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 contracts/capability/admission.go create mode 100644 dev/tasks/DEV-000115-cognitive-admission-control.json create mode 100644 tests/cognitive_admission/admission_test.go diff --git a/contracts/capability/admission.go b/contracts/capability/admission.go new file mode 100644 index 0000000..1d67164 --- /dev/null +++ b/contracts/capability/admission.go @@ -0,0 +1,68 @@ +package capability + +type AdmissionComplexity string +type AdmissionDecisionState string +type AdmissionReason string + +const ( + AdmissionSimple AdmissionComplexity = "SIMPLE" + AdmissionNormal AdmissionComplexity = "NORMAL" + AdmissionHeavy AdmissionComplexity = "HEAVY" + AdmissionAdmitted AdmissionDecisionState = "ADMITTED" + AdmissionRejected AdmissionDecisionState = "REJECTED" + AdmissionReasonNone AdmissionReason = "NONE" + AdmissionReasonConcurrency AdmissionReason = "CONCURRENCY_LIMIT" + AdmissionReasonAcceptedHour AdmissionReason = "ACCEPTED_PER_HOUR_LIMIT" + AdmissionReasonDeadline AdmissionReason = "DEADLINE" + AdmissionReasonInputBudget AdmissionReason = "INPUT_BUDGET" + AdmissionReasonOutputBudget AdmissionReason = "OUTPUT_BUDGET" +) + +type AdmissionPolicy struct { + ID string + MaxConcurrent int + MaxAcceptedPerHour int + MaxInputBytes int + MaxOutputBytes int +} + +type AdmissionRequest struct { + Complexity AdmissionComplexity + InputBytes int + MaxOutputBytes int + DeadlineMillis int64 +} + +type AdmissionCounters struct { + Concurrent int + AcceptedLastHour int +} + +type AdmissionDecision struct { + State AdmissionDecisionState + Reason AdmissionReason + PolicyID string +} + +func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision { + decision := AdmissionDecision{State: AdmissionAdmitted, Reason: AdmissionReasonNone, PolicyID: policy.ID} + if policy.ID == "" || policy.MaxConcurrent < 1 || policy.MaxAcceptedPerHour < 1 || policy.MaxInputBytes < 1 || policy.MaxOutputBytes < 1 { + return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID} + } + if counters.Concurrent >= policy.MaxConcurrent { + return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonConcurrency, PolicyID: policy.ID} + } + if counters.AcceptedLastHour >= policy.MaxAcceptedPerHour { + return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonAcceptedHour, PolicyID: policy.ID} + } + if request.InputBytes < 0 || request.InputBytes > policy.MaxInputBytes { + return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID} + } + if request.MaxOutputBytes < 1 || request.MaxOutputBytes > policy.MaxOutputBytes { + return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonOutputBudget, PolicyID: policy.ID} + } + if request.DeadlineMillis > 0 && request.DeadlineMillis <= nowMillis { + return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonDeadline, PolicyID: policy.ID} + } + return decision +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 40456eb..5694a52 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000114-run-liveness-assessment.json", - "task_id": "DEV-000114", + "task_file": "dev/tasks/DEV-000115-cognitive-admission-control.json", + "task_id": "DEV-000115", "task_revision": 1 } diff --git a/dev/tasks/DEV-000115-cognitive-admission-control.json b/dev/tasks/DEV-000115-cognitive-admission-control.json new file mode 100644 index 0000000..aa790d9 --- /dev/null +++ b/dev/tasks/DEV-000115-cognitive-admission-control.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000115", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "e83cf35", + "risk_tier": "E", + "intent": "Add a deterministic cognitive admission policy for concurrency, accepted/hour, deadline and input/output budgets without changing provider routing or bindings.", + "requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Explainable admission decision"], + "allowed_change_scope": {"modules": ["cognitive-admission"], "paths": ["contracts/capability/**", "modules/capability/domain/**", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "database/**", "migrations/**", "deploy/**", "security/**", "cmd/**", "runtime/**", "adapters/**", ".gitea/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0}, + "contracts": {"consume": ["Existing cognitive capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary"], "may_change": ["Typed admission request/policy/decision contract"]}, + "semantic_owner": "capability-cognitive-admission", + "canonical_state": [], + "state_transitions": ["request + policy + observed counters -> admitted or typed rejection"], + "side_effects_and_privileges": ["Pure policy evaluation; no provider call, queue, persistence, binding mutation or external mutation"], + "persistence_and_migrations": ["No schema or migration changes"], + "failure_retry_restart": ["Rejected requests carry a reason; retry requires a fresh policy evaluation"], + "assumptions": ["Counters are supplied by a later durable admission implementation", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"], + "acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "Simple/normal/heavy complexity remains a routing input, not authority"], + "required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "admission policy tests"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} diff --git a/tests/cognitive_admission/admission_test.go b/tests/cognitive_admission/admission_test.go new file mode 100644 index 0000000..4268a76 --- /dev/null +++ b/tests/cognitive_admission/admission_test.go @@ -0,0 +1,37 @@ +package cognitive_admission_test + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/capability" +) + +func TestAdmitReturnsExplainableDecision(t *testing.T) { + policy := capability.AdmissionPolicy{ID: "local-v1", MaxConcurrent: 2, MaxAcceptedPerHour: 10, MaxInputBytes: 1000, MaxOutputBytes: 500} + request := capability.AdmissionRequest{Complexity: capability.AdmissionNormal, InputBytes: 100, MaxOutputBytes: 100, DeadlineMillis: 2000} + if decision := capability.Admit(request, policy, capability.AdmissionCounters{}, 1000); decision.State != capability.AdmissionAdmitted || decision.PolicyID != policy.ID { + t.Fatalf("admitted = %#v", decision) + } + checks := []struct { + counters capability.AdmissionCounters + reason capability.AdmissionReason + }{ + {capability.AdmissionCounters{Concurrent: 2}, capability.AdmissionReasonConcurrency}, + {capability.AdmissionCounters{AcceptedLastHour: 10}, capability.AdmissionReasonAcceptedHour}, + } + for _, check := range checks { + if decision := capability.Admit(request, policy, check.counters, 1000); decision.State != capability.AdmissionRejected || decision.Reason != check.reason { + t.Fatalf("rejected = %#v want %s", decision, check.reason) + } + } +} + +func TestAdmitRejectsBudgetAndDeadline(t *testing.T) { + policy := capability.AdmissionPolicy{ID: "local-v1", MaxConcurrent: 1, MaxAcceptedPerHour: 1, MaxInputBytes: 10, MaxOutputBytes: 10} + if decision := capability.Admit(capability.AdmissionRequest{InputBytes: 11, MaxOutputBytes: 1}, policy, capability.AdmissionCounters{}, 1); decision.Reason != capability.AdmissionReasonInputBudget { + t.Fatalf("input decision = %#v", decision) + } + if decision := capability.Admit(capability.AdmissionRequest{InputBytes: 1, MaxOutputBytes: 1, DeadlineMillis: 1}, policy, capability.AdmissionCounters{}, 1); decision.Reason != capability.AdmissionReasonDeadline { + t.Fatalf("deadline decision = %#v", decision) + } +}