From 879de3bd95f58a7a34cf4351c6de3cb64e4b4955 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 11:41:52 +0300 Subject: [PATCH] Add typed context compiler v1 --- contracts/cognitive/context_compiler.go | 91 +++++++++++++++++++ dev/ACTIVE_TASK.json | 4 +- dev/tasks/DEV-000107-context-compiler-v1.json | 23 +++++ .../context_compiler/context_compiler_test.go | 59 ++++++++++++ 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 contracts/cognitive/context_compiler.go create mode 100644 dev/tasks/DEV-000107-context-compiler-v1.json create mode 100644 tests/context_compiler/context_compiler_test.go diff --git a/contracts/cognitive/context_compiler.go b/contracts/cognitive/context_compiler.go new file mode 100644 index 0000000..6b4d27e --- /dev/null +++ b/contracts/cognitive/context_compiler.go @@ -0,0 +1,91 @@ +package cognitive + +import ( + "errors" + "sort" + "strings" + + "git.cin.su/m/Assistant/contracts/capability" +) + +var ErrInvalidContextBudget = errors.New("context budget must be positive") + +type ContextCandidate struct { + Excerpt ContextExcerpt + Relevance int +} + +type ContextCompileInput struct { + Candidates []ContextCandidate + DataPolicy capability.DataPolicy + MaxBytes int +} + +type ContextOmission struct { + ID ContextExcerptID + Reason string +} + +type ContextBundle struct { + Excerpts []ContextExcerpt + Omissions []ContextOmission + Bytes int +} + +// CompileContext deterministically selects permitted, relevant excerpts for a +// gateway request. It carries no provider identity and performs no I/O. +func CompileContext(input ContextCompileInput) (ContextBundle, error) { + if input.MaxBytes <= 0 { + return ContextBundle{}, ErrInvalidContextBudget + } + candidates := append([]ContextCandidate(nil), input.Candidates...) + sort.SliceStable(candidates, func(i, j int) bool { + if candidates[i].Relevance != candidates[j].Relevance { + return candidates[i].Relevance > candidates[j].Relevance + } + return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID) + }) + bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}} + seen := make(map[ContextExcerptID]struct{}, len(candidates)) + for _, candidate := range candidates { + excerpt := candidate.Excerpt + if _, ok := seen[excerpt.ID]; ok { + bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "duplicate"}) + continue + } + seen[excerpt.ID] = struct{}{} + reason := permittedContextReason(excerpt, input.DataPolicy) + if reason != "" { + bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason}) + continue + } + if strings.TrimSpace(excerpt.Text) == "" { + bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"}) + continue + } + bytes := len([]byte(excerpt.Text)) + if bundle.Bytes+bytes > input.MaxBytes { + bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"}) + continue + } + bundle.Excerpts = append(bundle.Excerpts, excerpt) + bundle.Bytes += bytes + } + return bundle, nil +} + +func permittedContextReason(excerpt ContextExcerpt, policy capability.DataPolicy) string { + if excerpt.Sensitivity == SensitivitySecret { + return "secret" + } + if excerpt.Sensitivity == SensitivityUnknown { + return "unknown_sensitivity" + } + if policy == capability.DataPolicyExternalMessageOnly { + return "policy_message_only" + } + if policy == capability.DataPolicyExternalMinimizedContext && excerpt.Sensitivity != SensitivityPublic { + return "policy_public_only" + } + return "" +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 15878ef..f4e9f0d 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000106-governed-self-development-proposal.json", - "task_id": "DEV-000106", + "task_file": "dev/tasks/DEV-000107-context-compiler-v1.json", + "task_id": "DEV-000107", "task_revision": 1 } diff --git a/dev/tasks/DEV-000107-context-compiler-v1.json b/dev/tasks/DEV-000107-context-compiler-v1.json new file mode 100644 index 0000000..d7bf98f --- /dev/null +++ b/dev/tasks/DEV-000107-context-compiler-v1.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000107", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "3e0be62", + "risk_tier": "E", + "intent": "Implement a typed context compiler for relevance, sensitivity, provenance and byte budgets at the cognitive contract boundary.", + "requirement_ids": ["WP-101", "Typed context bundle", "Sensitivity/egress filtering and budget"], + "allowed_change_scope": {"modules": ["cognitive-contracts"], "paths": ["contracts/cognitive/**", "tests/context_compiler/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000107-context-compiler-v1.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "database/**", "migrations/**", "deploy/**", "security/**", "modules/**", "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 ContextExcerpt and Gateway request boundary", "Capability DataPolicy values"], "may_change": ["Cognitive context bundle/compiler contract"]}, + "semantic_owner": "cognitive-context-contract", + "canonical_state": [], + "state_transitions": ["candidate excerpts -> selected typed context bundle", "budget/sensitivity decision -> included or omitted with reason"], + "side_effects_and_privileges": ["Pure deterministic compilation; no provider call, persistence, external mutation or credential access"], + "persistence_and_migrations": ["No database schema or migration changes"], + "failure_retry_restart": ["Compilation is deterministic and repeatable; empty permitted context remains a valid bundle"], + "assumptions": ["Gateway remains responsible for provider selection", "Secret and unknown excerpts are never emitted", "Provider identity remains invocation provenance"], + "acceptance_scenarios": ["Relevant excerpts are selected deterministically", "Duplicate and low-value excerpts do not consume budget", "Secret/unknown data is omitted", "External minimized policy excludes private data", "UTF-8 byte budget is respected", "Provider identity is not represented as persona"], + "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", "context compiler tests"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} diff --git a/tests/context_compiler/context_compiler_test.go b/tests/context_compiler/context_compiler_test.go new file mode 100644 index 0000000..146a888 --- /dev/null +++ b/tests/context_compiler/context_compiler_test.go @@ -0,0 +1,59 @@ +package context_compiler_test + +import ( + "testing" + + "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/cognitive" +) + +func TestCompileContextOrdersDeduplicatesAndBudgets(t *testing.T) { + bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{ + MaxBytes: 4, + Candidates: []cognitive.ContextCandidate{ + {Excerpt: cognitive.ContextExcerpt{ID: "low", Sensitivity: cognitive.SensitivityPublic, Text: "low"}, Relevance: 1}, + {Excerpt: cognitive.ContextExcerpt{ID: "high", Sensitivity: cognitive.SensitivityPublic, Text: "éé"}, Relevance: 2}, + {Excerpt: cognitive.ContextExcerpt{ID: "high", Sensitivity: cognitive.SensitivityPublic, Text: "hi"}, Relevance: 3}, + }, + }) + if err != nil { + t.Fatal(err) + } + if len(bundle.Excerpts) != 1 || bundle.Excerpts[0].ID != "high" || bundle.Bytes != 2 { + t.Fatalf("unexpected bundle: %#v", bundle) + } + if len(bundle.Omissions) != 2 || bundle.Omissions[0].Reason != "duplicate" || bundle.Omissions[1].Reason != "budget" { + t.Fatalf("unexpected omissions: %#v", bundle.Omissions) + } +} + +func TestCompileContextFiltersSensitivityByPolicy(t *testing.T) { + bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{ + MaxBytes: 100, + DataPolicy: capability.DataPolicyExternalMinimizedContext, + Candidates: []cognitive.ContextCandidate{ + {Excerpt: cognitive.ContextExcerpt{ID: "public", Sensitivity: cognitive.SensitivityPublic, Text: "ok"}}, + {Excerpt: cognitive.ContextExcerpt{ID: "private", Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "private"}}, + {Excerpt: cognitive.ContextExcerpt{ID: "secret", Sensitivity: cognitive.SensitivitySecret, Text: "secret"}}, + }, + }) + if err != nil { + t.Fatal(err) + } + if len(bundle.Excerpts) != 1 || bundle.Excerpts[0].ID != "public" { + t.Fatalf("unexpected excerpts: %#v", bundle.Excerpts) + } + if len(bundle.Omissions) != 2 { + t.Fatalf("unexpected omissions: %#v", bundle.Omissions) + } +} + +func TestCompileContextAllowsEmptyResultAndRejectsInvalidBudget(t *testing.T) { + if _, err := cognitive.CompileContext(cognitive.ContextCompileInput{}); err == nil { + t.Fatal("expected invalid budget") + } + bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 10, Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "secret", Sensitivity: cognitive.SensitivitySecret, Text: "x"}}}}) + if err != nil || len(bundle.Excerpts) != 0 || len(bundle.Omissions) != 1 { + t.Fatalf("unexpected empty result: %#v %v", bundle, err) + } +}