Compare commits

...
Author SHA1 Message Date
m 754cbe43d2 wip(dev-000073): preserve durable bootstrap checkpoint
CI / test (push) Failing after 19s
DEV-000073 is NOT DONE; current work is partial/stabilized WIP only. go test ./cmd/devctl was last verified PASS. Remaining bootstrap/secret integration requires a stronger long-horizon model. This commit is preservation-only; current ancestry includes DEV-000072 candidate 36fec6c05cdf7aaf413fe... and must be reconciled before DEV-000073 promotion.
2026-08-18 12:07:11 +03:00
m 36fec6c05c DEV-000072: classify identity naming preference as directive and avoid assertion
CI / test (push) Successful in 18s
2026-08-18 02:15:49 +03:00
m 11a6a23f6e Merge pull request 'DEV-000072 semantic interpretation deferral boundary' (#69) from codex/dev-000072-semantic-deferral-boundary-v1 into main
CI / test (push) Successful in 18s
2026-08-17 20:03:08 +00:00
m ce666bbc43 DEV-000072 semantic interpretation deferral boundary
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
2026-08-17 23:02:00 +03:00
m 64870798b9 Merge pull request 'DEV-000071 cognitive path reality and generalization baseline' (#68) from codex/dev-000071-promotion into main
CI / test (push) Successful in 17s
2026-08-17 19:33:08 +00:00
33 changed files with 4855 additions and 125 deletions
+7
View File
@@ -35,6 +35,13 @@ Do not silently resolve a conflict. Return `BLOCKED_BY_SPEC_GAP` or `BLOCKED_BY_
- NO reading/copying literal passwords from other repositories into this repository.
- NO production secret in source, task packet, commit message, logs or tests.
## Deployment safety (production mutation)
- Production mutation in normal `devctl` workflow must go through `deploy prepare` then `deploy apply`.
- `deploy apply` is rejected when required inputs are `MISSING`, `EMPTY`, `INVALID`, `UNKNOWN`, `UNRESOLVED`, or `STALE`.
- Ambient shell state is not accepted as authoritative required production input; required values must come from an explicit durable source (for example `--env-file`/manifest-classified bootstrap path).
- A prepared plan must be current (inputs, compose, context, revision binding) before apply proceeds.
## Codex freedom
Inside the declared owner/module and allowed paths, choose ordinary local implementation details freely. Prefer explicit, typed, boring Go over reflection, metaprogramming or framework tricks.
+12 -2
View File
@@ -43,15 +43,25 @@ func (r *InteractionRepository) AppendUtterance(ctx context.Context, c interacti
return tx.Commit(ctx)
}
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
func (r *InteractionRepository) SaveSemanticResult(ctx context.Context, interpretation interaction.UtteranceInterpretation, candidates []interaction.SemanticCandidate, projects []interaction.ProjectReference, relations []interaction.InteractionRelation) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.RecordedAtUnixMillis); err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.utterance_interpretations(interpretation_id,utterance_id,speech_act,secondary_speech_act,confidence_millis,source,assertion_verification,requested_outcome,clarification,needs_cognition,applicability,selection_reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`, interpretation.ID, interpretation.UtteranceID, interpretation.SpeechAct, interpretation.SecondarySpeechAct, interpretation.ConfidenceMillis, interpretation.Source, interpretation.AssertionVerification, interpretation.RequestedOutcome, interpretation.Clarification, interpretation.NeedsCognition, interpretation.Applicability, interpretation.SelectionReason, interpretation.RecordedAtUnixMillis); err != nil {
return err
}
for _, candidate := range candidates {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.semantic_candidates(candidate_id,utterance_id,source,selection_state,primary_speech_act,secondary_speech_act,confidence_millis,applicability,requested_outcome,clarification,needs_cognition,selection_reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(candidate_id) DO UPDATE SET selection_state=EXCLUDED.selection_state,primary_speech_act=EXCLUDED.primary_speech_act,secondary_speech_act=EXCLUDED.secondary_speech_act,confidence_millis=EXCLUDED.confidence_millis,applicability=EXCLUDED.applicability,requested_outcome=EXCLUDED.requested_outcome,clarification=EXCLUDED.clarification,needs_cognition=EXCLUDED.needs_cognition,selection_reason=EXCLUDED.selection_reason,recorded_at_unix_millis=EXCLUDED.recorded_at_unix_millis`, candidate.ID, candidate.UtteranceID, candidate.Source, candidate.SelectionState, candidate.PrimarySpeechAct, candidate.SecondarySpeechAct, candidate.ConfidenceMillis, candidate.Applicability, candidate.RequestedOutcome, candidate.Clarification, candidate.NeedsCognition, candidate.SelectionReason, candidate.RecordedAtUnixMillis); err != nil {
return err
}
for _, hint := range candidate.CapabilityHints {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.semantic_candidate_capability_hints(candidate_id,capability_kind) VALUES($1,$2) ON CONFLICT DO NOTHING`, candidate.ID, hint); err != nil {
return err
}
}
}
for _, project := range projects {
if _, err = tx.Exec(ctx, `INSERT INTO interaction.project_references(project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, project.ID, project.UtteranceID, project.ProjectID, project.ProjectDisplayName, project.Role, project.Provenance, project.ConfidenceMillis, project.RecordedAtUnixMillis); err != nil {
return err
+1
View File
@@ -19,6 +19,7 @@ These questions are intentionally unresolved. Codex must not silently turn tempo
| UQ-013 | Multi-user organization governance | LATER | Actor/Principal/Scope designed generally; expose single-user V1 if desired | No | Actual shared workspace requirement |
| UQ-014 | Canonical wire codec longevity | RESEARCH | Versioned schema/envelope; initial protobuf acceptable, not eternal identity | No | First non-Go module or major semantic migration |
| UQ-015 | Exact data retention/privacy policy engine | RESEARCH | Store scope/provenance/sensitivity hooks; implement concrete policies per product deployment | No | Product/legal requirements |
| UQ-016 | Generic memory/knowledge admission from ordinary interaction | RESEARCH | Treat ordinary preferences/preferences-like assertions as candidates first; no predefined per-topic memory schemas | No | When a durable knowledge/persistence mechanism is adopted with explicit policy and owner boundaries |
## 7.1 Spec-gap protocol
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayInterpretationRequest
if !decodeJSON(w, r, 80<<10, &request) {
return
}
if request.Version != cognitive.CreatorAdvisoryV1 || request.RequestID == "" || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || strings.TrimSpace(request.Message) == "" || request.MaxOutputBytes < 1 {
http.Error(w, "invalid interpretation request", http.StatusBadRequest)
return
}
maxOutputBytes := request.MaxOutputBytes
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{
CapabilityID: creatorAdvisoryCapabilityID,
OwnerPrincipalID: request.PrincipalID,
DataPolicy: request.DataPolicy,
RequiredModalities: []capability.ModelModality{capability.ModalityText},
RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory},
AllowCanary: g.allowCanary,
InputBytes: int64(len(request.Message)),
MaxOutputBytes: int64(maxOutputBytes),
}
selected, err := g.resolver.Resolve(r.Context(), requirement)
if err != nil {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
return
}
provider := g.provider(selected)
if provider == nil {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
return
}
invocation := cognitive.AdvisoryRequest{
Version: request.Version,
RequestID: request.RequestID,
ActorID: request.ActorID,
PrincipalID: request.PrincipalID,
ScopeID: request.ScopeID,
Message: interpretationPrompt(request),
Locale: request.Locale,
MaxOutputBytes: maxOutputBytes,
ProviderBindingID: selected.Binding.ID,
ProviderBindingRevision: selected.Binding.Revision,
ProviderInstanceID: selected.Provider.ID,
ModelDescriptorID: selected.Model.ID,
ProviderModelID: selected.Model.ProviderModelID,
}
started := g.now()
response, invokeErr := provider.Advise(r.Context(), invocation)
if invokeErr != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observe(r.Context(), selected, response, started)
if response.FinishReason != cognitive.FinishCompleted {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
return
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
return
}
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
}
func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string {
projects := "none"
if len(request.ProjectNames) > 0 {
projects = strings.Join(request.ProjectNames, ", ")
}
return fmt.Sprintf(`Return only strict JSON with keys primary_speech_act, secondary_speech_act, confidence_millis, applicability, requested_outcome, needs_cognition, clarification, selection_reason, capability_hints.
Allowed speech acts: DIRECTIVE, ASSERTION, DECISION, PREFERENCE, QUESTION, HYPOTHESIS, CORRECTION, REQUEST_TO_INVESTIGATE, CONFIRMATION, REJECTION, CANCELLATION, CONVERSATIONAL, UNKNOWN.
Allowed applicability: APPLICABLE, LOW_CONFIDENCE, MIXED_INTENT, NEEDS_BROADER_INTERPRETATION, CLARIFICATION_REQUIRED.
Allowed clarification: NOT_REQUIRED or REQUIRED.
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
Interpret the human utterance semantically. Do not claim action execution. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
Message: %q
Deterministic primary: %s
Deterministic secondary: %s
Deterministic confidence: %d
Deterministic applicability: %s
Deterministic requested outcome: %q
Relevant project names: %s`, request.Message, request.DeterministicSpeechAct, request.DeterministicSecondary, request.DeterministicConfidence, request.DeterministicApplicability, request.RequestedOutcome, projects)
}
func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationCandidate, bool) {
start := strings.Index(text, "{")
end := strings.LastIndex(text, "}")
if start < 0 || end <= start {
return cognitive.GatewayInterpretationCandidate{}, false
}
var candidate cognitive.GatewayInterpretationCandidate
if err := json.Unmarshal([]byte(text[start:end+1]), &candidate); err != nil {
return cognitive.GatewayInterpretationCandidate{}, false
}
if !validSpeechAct(candidate.PrimarySpeechAct) || candidate.ConfidenceMillis < 0 || candidate.ConfidenceMillis > 1000 {
return cognitive.GatewayInterpretationCandidate{}, false
}
if candidate.SecondarySpeechAct != "" && !validSpeechAct(candidate.SecondarySpeechAct) {
return cognitive.GatewayInterpretationCandidate{}, false
}
switch candidate.Applicability {
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
default:
return cognitive.GatewayInterpretationCandidate{}, false
}
if candidate.Clarification != "NOT_REQUIRED" && candidate.Clarification != "REQUIRED" {
return cognitive.GatewayInterpretationCandidate{}, false
}
return candidate, true
}
func validSpeechAct(value string) bool {
switch value {
case "DIRECTIVE", "ASSERTION", "DECISION", "PREFERENCE", "QUESTION", "HYPOTHESIS", "CORRECTION", "REQUEST_TO_INVESTIGATE", "CONFIRMATION", "REJECTION", "CANCELLATION", "CONVERSATIONAL", "UNKNOWN":
return true
}
return false
}
func writeInterpretationResponse(w http.ResponseWriter, response cognitive.GatewayInterpretationResponse) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}
@@ -0,0 +1,219 @@
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/situation"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestLiveInterpretationTrace(t *testing.T) {
if os.Getenv("ASSISTANT_LIVE_SEMANTIC_TRACE") != "1" {
t.Skip("set ASSISTANT_LIVE_SEMANTIC_TRACE=1 for live semantic/gateway/raw trace")
}
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
rawBase := envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1")
rawModelID, err := firstRawModel(ctx, rawBase)
if err != nil {
t.Fatal(err)
}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(rawBase, nil)
if err != nil {
t.Fatal(err)
}
discovered, probe := cpuAdapter.Discover(ctx)
if probe.Health != capability.HealthAvailable || len(discovered) == 0 {
t.Fatalf("cpu discovery unavailable: probe=%+v models=%+v", probe, discovered)
}
selected := capability.ResolvedCognitiveBinding{
Binding: capability.CognitiveBinding{
ID: "trace-binding",
Revision: 1,
CapabilityID: "creator-advisory",
OwnerPrincipalID: "domovoy-creator-principal",
DataPolicy: capability.DataPolicyLocalOnly,
},
Provider: capability.ProviderInstance{
ID: "local-cpu-primary",
Revision: 1,
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-cpu-primary-v1",
},
Model: capability.ModelDescriptor{
ID: capability.ModelDescriptorID("trace-model"),
Revision: 1,
ProviderModelID: discovered[0].ProviderModelID,
},
}
gateway := cognitiveGateway{
resolver: traceResolver{selected: selected},
providers: map[capability.ProviderKind][]providerRegistration{
"openai-compatible-chat-v1": {{
ProviderInstanceID: "local-cpu-primary",
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-cpu-primary-v1",
Provider: cpuAdapter,
}},
},
now: time.Now,
allowCanary: true,
}
for _, message := range []string{"ты кто", "можешь расказать об утре"} {
conversation := interaction.Conversation{ID: "trace-conversation", ActorID: "trace-actor", PrincipalID: "domovoy-creator-principal", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
utterance, _, _, err := interactiondomain.NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message}, time.Now())
if err != nil {
t.Fatal(err)
}
semantic, err := interactiondomain.InterpretUtterance(utterance, []situation.ProjectDescriptor{{ID: "assistant", DisplayName: "Assistant"}}, nil, time.Now())
if err != nil {
t.Fatal(err)
}
rawResponse, err := rawCompletion(ctx, rawBase, rawModelID, message)
if err != nil {
t.Fatal(err)
}
request := cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "trace-request",
ActorID: identity.ActorID("trace-actor"),
PrincipalID: identity.PrincipalID("domovoy-creator-principal"),
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: message,
Locale: "ru",
MaxOutputBytes: 4096,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
}
payload, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
httpRequest := httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload))
gateway.interpretationHandler(recorder, httpRequest.WithContext(ctx))
if recorder.Code != http.StatusOK {
t.Fatalf("message=%q gateway status=%d body=%s", message, recorder.Code, recorder.Body.String())
}
var gatewayResponse cognitive.GatewayInterpretationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &gatewayResponse); err != nil {
t.Fatal(err)
}
if gatewayResponse.FinishReason != cognitive.FinishCompleted {
t.Fatalf("message=%q gateway finish=%s body=%s", message, gatewayResponse.FinishReason, recorder.Body.String())
}
if rawResponse == "" {
t.Fatalf("message=%q raw response is empty", message)
}
if message == "ты кто" {
if semantic.Interpretation.SpeechAct != interaction.SpeechActQuestion || !semantic.NeedsCognition {
t.Fatalf("semantic path still fails punctuation-free identity question: %+v", semantic.Interpretation)
}
if gatewayResponse.Candidate.PrimarySpeechAct != "QUESTION" {
t.Fatalf("gateway did not preserve question semantics: %+v", gatewayResponse.Candidate)
}
}
t.Logf("TRACE message=%q deterministic=%s/%s needs_cognition=%t raw=%q gateway_candidate=%+v", message, semantic.Interpretation.SpeechAct, semantic.Interpretation.Applicability, semantic.NeedsCognition, truncate(rawResponse, 180), gatewayResponse.Candidate)
}
}
type traceResolver struct {
selected capability.ResolvedCognitiveBinding
}
func (r traceResolver) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
return r.selected, nil
}
func (r traceResolver) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (r traceResolver) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
func firstRawModel(ctx context.Context, base string) (string, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/models", nil)
if err != nil {
return "", err
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&payload); err != nil {
return "", err
}
if len(payload.Data) == 0 || payload.Data[0].ID == "" {
return "", io.EOF
}
return payload.Data[0].ID, nil
}
func rawCompletion(ctx context.Context, base, model, message string) (string, error) {
payload, err := json.Marshal(map[string]any{
"model": model,
"max_tokens": 128,
"messages": []map[string]string{
{"role": "user", "content": message},
},
"stream": false,
})
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, base+"/chat/completions", bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
return "", err
}
if len(result.Choices) == 0 {
return "", io.EOF
}
return result.Choices[0].Message.Content, nil
}
func truncate(value string, max int) string {
if len(value) <= max {
return value
}
return value[:max] + "..."
}
+1
View File
@@ -63,6 +63,7 @@ func main() {
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
log.Printf("cognitive-worker listening on %s", addr)
@@ -0,0 +1,125 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
"git.cin.su/m/Assistant/platform/ids"
)
type cognitiveInterpretationClient struct {
baseURL *url.URL
client *http.Client
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
return cognitiveInterpretationClient{}, fmt.Errorf("invalid Cognitive Runtime URL")
}
if client == nil {
client = http.DefaultClient
}
return cognitiveInterpretationClient{baseURL: base, client: client}, nil
}
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
target := *c.baseURL
target.Path = "/v1/interpretations"
payload, err := json.Marshal(request)
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
upstream.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(upstream)
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return cognitive.GatewayInterpretationResponse{}, fmt.Errorf("cognitive interpretation status %d", response.StatusCode)
}
var result cognitive.GatewayInterpretationResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
return result, nil
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
switch result.Interpretation.Applicability {
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
return true
default:
return false
}
}
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
requestID, _ := ids.New()
return cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID,
PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: utterance.Content,
Locale: "ru",
MaxOutputBytes: 4096,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
ProjectNames: projectNames,
}
}
func interactionCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) {
id, err := ids.New()
if err != nil {
return interaction.SemanticCandidate{}, err
}
return interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(id),
UtteranceID: utteranceID,
Source: interaction.CandidateSourceCognitive,
SelectionState: interaction.CandidateSelected,
PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct),
SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct),
ConfidenceMillis: candidate.ConfidenceMillis,
Applicability: interaction.SemanticApplicability(candidate.Applicability),
RequestedOutcome: candidate.RequestedOutcome,
Clarification: interaction.ClarificationState(candidate.Clarification),
NeedsCognition: candidate.NeedsCognition,
SelectionReason: candidate.SelectionReason,
CapabilityHints: capabilityHintStrings(candidate.CapabilityHints),
RecordedAtUnixMillis: now.UnixMilli(),
}, nil
}
func capabilityHintStrings(values []capability.CapabilityKind) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
result = append(result, string(value))
}
return result
}
+35 -1
View File
@@ -8,6 +8,7 @@ import (
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
@@ -111,6 +112,10 @@ func main() {
if err != nil {
log.Fatal(err)
}
cognitiveClient, err := newCognitiveInterpretationClient(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), &http.Client{Timeout: 20 * time.Second})
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
@@ -288,11 +293,24 @@ func main() {
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
return
}
if shouldRequestBroaderInterpretation(semantic) {
projectNames := make([]string, 0, len(semantic.Projects))
for _, ref := range semantic.Projects {
if ref.ProjectDisplayName != "" {
projectNames = append(projectNames, ref.ProjectDisplayName)
}
}
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, utterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, candidate, previousFocus)
}
}
}
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
http.Error(w, "save chat message", http.StatusInternalServerError)
return
}
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Projects, semantic.Relations); err != nil {
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Candidates, semantic.Projects, semantic.Relations); err != nil {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
@@ -349,7 +367,11 @@ func main() {
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
Message: utterance.Content,
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
SecondarySpeechAct: work.SpeechActRef(semantic.Interpretation.SecondarySpeechAct),
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
NeedsCognition: semantic.NeedsCognition,
RequiresClarification: semantic.Interpretation.Clarification == interaction.ClarificationRequired,
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
@@ -1087,6 +1109,18 @@ func humanCapabilityList(kinds []capability.CapabilityKind) string {
return strings.Join(labels, ", ")
}
func toCapabilityKinds(values []string) []capability.CapabilityKind {
result := make([]capability.CapabilityKind, 0, len(values))
for _, value := range values {
kind := capability.CapabilityKind(strings.TrimSpace(value))
if kind == "" {
continue
}
result = append(result, kind)
}
return result
}
func delegationAttentionRef(id work.DelegationID) string {
return "work:delegation:" + string(id)
}
+1611
View File
File diff suppressed because it is too large Load Diff
+644
View File
@@ -0,0 +1,644 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
)
func TestDiscoverComposeInputsRequiredAndOptional(t *testing.T) {
content := `A=${POSTGRES_SUPERUSER}
B=${POSTGRES_DB:-assistant}
C=${CREATOR_PASSWORD_FILE}
D=${FOO_CREDENTIAL_REF}
`
inputs := discoverComposeInputs(content)
if _, ok := inputs["POSTGRES_SUPERUSER"]; !ok {
t.Fatal("POSTGRES_SUPERUSER should be discovered")
}
if !inputs["POSTGRES_SUPERUSER"].required {
t.Fatalf("POSTGRES_SUPERUSER expected required")
}
if inputs["POSTGRES_DB"].required {
t.Fatalf("POSTGRES_DB expected optional")
}
if !inputs["CREATOR_PASSWORD_FILE"].required {
t.Fatalf("CREATOR_PASSWORD_FILE expected required from compose placeholder")
}
if !inputs["EPISTEMICS_POSTGRES_CREDENTIAL_REF"].required {
t.Fatalf("required production bootstrap variable should be required")
}
if got := len(inputs); got < 4 {
t.Fatalf("expected at least 4 discovered inputs, got %d", got)
}
}
func TestResolveDeploymentInputsRejectsAmbientProduction(t *testing.T) {
t.Setenv("POSTGRES_SUPERUSER", "from_ambient")
inputs := map[string]composeInput{
"POSTGRES_SUPERUSER": {name: "POSTGRES_SUPERUSER", required: true},
}
resolved, _, _, err := resolveDeploymentInputs(inputs, "", true)
if err != nil {
t.Fatalf("resolveDeploymentInputs error: %v", err)
}
if len(resolved) != 1 {
t.Fatalf("expected one resolved input, got %d", len(resolved))
}
if resolved[0].Source != "ambient" || resolved[0].Resolved {
t.Fatalf("expected unresolved ambient resolution, got source=%s resolved=%v", resolved[0].Source, resolved[0].Resolved)
}
}
func TestDeployPrepareRequiresEnvFileForProduction(t *testing.T) {
root := t.TempDir()
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
err := deployPrepare([]string{"--target", "production", "--compose", composeFile})
if err == nil || !strings.Contains(err.Error(), "production prepare requires --env-file") {
t.Fatalf("expected missing env-file failure, got %v", err)
}
}
func TestDeployPrepareBlockedAndApplyRefusesMissingRequiredInput(t *testing.T) {
root := t.TempDir()
repoRoot, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(root, "production.env")
parts := make([]string, 0, len(requiredChatOnlyInputs))
for key := range requiredChatOnlyInputs {
if key == "POSTGRES_SUPERUSER" {
continue
}
parts = append(parts, key+"=value-"+key)
}
if err := os.WriteFile(envFile, []byte(strings.Join(parts, "\n")), 0o600); err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
target.ComposeFile = composeFile
plan, err := buildDeploymentPlan(repoRoot, target, envFile, "HEAD", 24, "")
if err != nil {
t.Fatal(err)
}
if plan.Readiness != readinessFailed {
t.Fatalf("expected blocked plan, got readiness=%s", plan.Readiness)
}
if !strings.Contains(strings.Join(plan.ReadinessReasons, "\n"), "POSTGRES_SUPERUSER") {
t.Fatalf("expected missing required input blocker for POSTGRES_SUPERUSER, got %v", plan.ReadinessReasons)
}
planPath := filepath.Join(root, "blocked-plan.json")
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, payload, 0o600); err != nil {
t.Fatal(err)
}
if err := deployApply([]string{"-plan", planPath}); err == nil {
t.Fatal("expected apply refusal for blocked plan")
}
}
func TestAssertPlanCurrentRejectsStaleInputChange(t *testing.T) {
root := t.TempDir()
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(root, "deploy.env")
if err := os.WriteFile(envFile, []byte("POSTGRES_SUPERUSER=first\n"), 0o600); err != nil {
t.Fatal(err)
}
inputs, err := discoverComposeInputsFromFile(composeFile)
if err != nil {
t.Fatal(err)
}
resolved, _, _, err := resolveDeploymentInputs(inputs, envFile, true)
if err != nil {
t.Fatal(err)
}
plan := &deploymentPlan{
Version: 1,
PlanID: "stale-test",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
Readiness: readinessReady,
ReadinessReasons: nil,
Target: "production",
ComposeFile: composeFile,
DockerContext: "docker-prod",
ProjectName: "assistant",
EnvFile: envFile,
SourceRevision: "11a6a23f6e4f44dd88265a04846419a27f54df98",
SourceRevisionMode: "explicit",
ArtifactRevision: "11a6a23f6e4f44dd88265a04846419a27f54df98",
DeploymentConfigRev: resolveConfigHash(composeFile),
CurrentProductionRev: "",
KnownGoodRevision: "",
Inputs: resolved,
Classify: map[string][]string{},
KnownInputsSource: map[string]string{},
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
resolved,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
if err := os.WriteFile(envFile, []byte("POSTGRES_SUPERUSER=second\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := assertPlanCurrent(plan); err == nil {
t.Fatal("expected stale plan after input change")
}
}
func TestAssertPlanCurrentRejectsArtifactMismatch(t *testing.T) {
root := t.TempDir()
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(root, "deploy.env")
envParts := make([]string, 0, len(requiredChatOnlyInputs))
for key := range requiredChatOnlyInputs {
envParts = append(envParts, key+"=value-"+strings.ToLower(key))
}
envParts = append(envParts, "POSTGRES_SUPERUSER=from_file")
if err := os.WriteFile(envFile, []byte(strings.Join(envParts, "\n")), 0o600); err != nil {
t.Fatal(err)
}
inputs, err := discoverComposeInputsFromFile(composeFile)
if err != nil {
t.Fatal(err)
}
discovered, _, _, err := resolveDeploymentInputs(inputs, envFile, true)
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
configRev := resolveConfigHash(composeFile)
shaA := strings.Repeat("a", 40)
shaB := strings.Repeat("b", 40)
plan := &deploymentPlan{
Version: 1,
PlanID: "artifact-mismatch",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
Readiness: readinessReady,
Target: "production",
ComposeFile: composeFile,
DockerContext: "docker-prod",
ProjectName: "assistant",
EnvFile: envFile,
SourceRevision: shaA,
SourceRevisionMode: "explicit",
ArtifactRevision: shaA,
DeploymentConfigRev: configRev,
Inputs: discovered,
Classify: map[string][]string{},
KnownInputsSource: map[string]string{},
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
discovered,
shaA,
shaA,
configRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
plan.ArtifactRevision = shaB
plan.SourceRevision = shaB
if err := assertPlanCurrent(plan); err == nil {
t.Fatal("expected stale plan due artifact mismatch")
}
}
func TestMaxMigrationPrefixFromListHandlesNestedMigrationPaths(t *testing.T) {
got := maxMigrationPrefixFromList([]string{
"database/migrations/001_bootstrap.sql",
"database/migrations/007_legacy.sql",
"database/migrations/work/015_capability.sql",
"database/migrations/README.md",
"database/migrations/artifact/019_work.sql",
})
if got != "19" {
t.Fatalf("expected max migration prefix 19, got %q", got)
}
}
func TestAssertPlanCurrentRejectsMigrationUpgradeWithoutKnownGood(t *testing.T) {
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
headOut, err := runGit(root, "rev-parse", "HEAD")
if err != nil {
t.Fatal(err)
}
targetRevision := strings.TrimSpace(string(headOut))
targetMaxStr, err := maxMigrationPrefixForRevision(root, targetRevision)
if err != nil {
t.Fatal(err)
}
targetMax, err := strconv.Atoi(targetMaxStr)
if err != nil {
t.Fatalf("invalid target migration max %q: %v", targetMaxStr, err)
}
revisionsOut, err := runGit(root, "rev-list", "HEAD", "--max-count", "20")
if err != nil {
t.Fatal(err)
}
ancestorList := strings.Fields(string(revisionsOut))
currentProductionRevision := ""
for _, revision := range ancestorList {
if strings.TrimSpace(revision) == targetRevision {
continue
}
revMaxStr, err := maxMigrationPrefixForRevision(root, revision)
if err != nil {
continue
}
revMax, err := strconv.Atoi(revMaxStr)
if err != nil {
continue
}
if revMax < targetMax {
currentProductionRevision = revision
break
}
}
if currentProductionRevision == "" {
t.Skip("could not find historical revision with lower migration max to validate precondition block")
}
rootDir := t.TempDir()
composeFile := filepath.Join(rootDir, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}\n"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(rootDir, "bootstrap.env")
requiredValues := make([]string, 0, len(requiredChatOnlyInputs))
for key := range requiredChatOnlyInputs {
requiredValues = append(requiredValues, key+"=value-"+strings.ToLower(key))
}
if err := os.WriteFile(envFile, []byte(strings.Join(requiredValues, "\n")), 0o600); err != nil {
t.Fatal(err)
}
inputs, err := discoverComposeInputsFromFile(composeFile)
if err != nil {
t.Fatal(err)
}
discovered, _, _, err := resolveDeploymentInputs(inputs, envFile, true)
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
target.ComposeFile = composeFile
plan := &deploymentPlan{
Version: 1,
PlanID: "migration-precondition",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
Readiness: readinessReady,
ReadinessReasons: nil,
Target: "production",
ComposeFile: composeFile,
DockerContext: target.DockerContext,
ProjectName: target.ProjectName,
EnvFile: envFile,
SourceRevision: targetRevision,
SourceRevisionMode: "explicit",
ArtifactRevision: targetRevision,
DeploymentConfigRev: resolveConfigHash(composeFile),
CurrentProductionRev: currentProductionRevision,
KnownGoodRevision: "",
Classify: map[string][]string{},
Inputs: discovered,
KnownInputsSource: map[string]string{},
}
plan.Migrations, err = migrationState(root, targetRevision, currentProductionRevision)
if err != nil {
t.Fatal(err)
}
if !plan.Migrations.NeedKnownGood {
t.Fatal("expected migration state to require known good for this test")
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
plan.Inputs,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
if err := assertPlanCurrent(plan); err == nil {
t.Fatal("expected assertPlanCurrent to reject migration precondition without known-good")
} else if !strings.Contains(err.Error(), "migration precondition failed") {
t.Fatalf("expected migration precondition failure, got: %v", err)
}
}
func TestDeploymentPlanDoesNotLeakSecretValues(t *testing.T) {
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(t.TempDir(), "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}\n"), 0o600); err != nil {
t.Fatal(err)
}
target.ComposeFile = composeFile
envFile := filepath.Join(t.TempDir(), "production.env")
if err := os.WriteFile(envFile, []byte(strings.Join([]string{
"POSTGRES_SUPERUSER_PASSWORD=super-secret-123",
"CREATOR_PASSWORD_FILE=creator-password-file-456",
"COMMIT_EXECUTOR_TOKEN=executor-token-789",
}, "\n")), 0o600); err != nil {
t.Fatal(err)
}
plan, err := buildDeploymentPlan(root, target, envFile, "HEAD", 24, "")
if err != nil {
t.Fatal(err)
}
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
serialized := string(payload)
for _, secretValue := range []string{
"super-secret-123",
"creator-password-file-456",
"executor-token-789",
} {
if strings.Contains(serialized, secretValue) {
t.Fatalf("plain secret value leaked into plan: %q", secretValue)
}
}
if got := len(plan.SecretReferences); got < 3 {
t.Fatalf("expected at least 3 secret references in plan, got %d", got)
}
}
func TestDefaultKnownGoodManifestPathStableWithoutLocalAppData(t *testing.T) {
t.Setenv("LOCALAPPDATA", "")
t.Setenv("LOCALAPPDATA_ALT", "")
if strings.TrimSpace(defaultKnownGoodManifestPath()) != "" {
t.Fatal("expected empty path when LOCALAPPDATA is unset")
}
}
func TestDeployApplyRunsHealthGateWithReadyPlanAndSimulatedComposeDocker(t *testing.T) {
originalPath := os.Getenv("PATH")
fakeDockerDir := t.TempDir()
fakeDocker := filepath.Join(fakeDockerDir, "docker.bat")
dockerBatch := "@echo off\n" +
"if \"%~1\"==\"--context\" (\n" +
" if \"%~3\"==\"compose\" exit /B 0\n" +
" if \"%~3\"==\"inspect\" (\n" +
" if \"%~6\"==\"{{index .Config.Labels \\\"org.opencontainers.image.revision\\\"}}\" echo %FAKE_DOCKER_REVISION%\n" +
" if \"%~6\"==\"{{.State.Status}}\" echo running\n" +
" exit /B 0\n" +
" )\n" +
" if \"%~3\"==\"volume\" if \"%~4\"==\"inspect\" exit /B 0\n" +
")\n" +
"exit /B 1\n"
if err := os.WriteFile(fakeDocker, []byte(dockerBatch), 0o600); err != nil {
t.Fatal(err)
}
os.Setenv("PATH", fakeDockerDir+";"+originalPath)
defer os.Setenv("PATH", originalPath)
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(fakeDockerDir, "docker-compose.prod.yml")
target.ComposeFile = composeFile
if err := os.WriteFile(composeFile, []byte("services:\\n app:\\n image: alpine\\n environment:\\n - POSTGRES_SUPERUSER=${POSTGRES_SUPERUSER}\\n"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(fakeDockerDir, "production.env")
env := []string{
"POSTGRES_SUPERUSER=deploy-apply-test",
"POSTGRES_SUPERUSER_PASSWORD=pw-super",
"CONTINUITY_DB_PASSWORD=pw-cont",
"WORK_DB_PASSWORD=pw-work",
"CAPABILITY_DB_PASSWORD=pw-cap",
"COMMIT_DB_PASSWORD=pw-commit",
"AUTHORITY_DB_PASSWORD=pw-auth",
"IDENTITY_DB_PASSWORD=pw-id",
"SITUATION_DB_PASSWORD=pw-sit",
"CREATOR_USERNAME=creator",
"CREATOR_PASSWORD_FILE=creator-password",
"CREATOR_BOOTSTRAP_TOKEN=creator-bootstrap",
"CREATOR_SESSION_SIGNING_KEY=creator-session",
"COMMIT_EXECUTOR_TOKEN=commit-exec",
"EPISTEMICS_POSTGRES_CREDENTIAL_REF=epi-cred-ref",
"INTERACTION_POSTGRES_CREDENTIAL_REF=int-cred-ref",
"EPISTEMICS_POSTGRES_PASSWORD=epi-password",
"INTERACTION_POSTGRES_PASSWORD=int-password",
}
if err := os.WriteFile(envFile, []byte(strings.Join(env, "\n")), 0o600); err != nil {
t.Fatal(err)
}
knownGood, _, err := captureKnownGood(target, "")
if err != nil || strings.TrimSpace(knownGood) == "" {
knownGood = "11a6a23f6e4f44dd88265a04846419a27f54df98"
}
plan, err := buildDeploymentPlan(root, target, envFile, knownGood, 24, "")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusOK)
}))
defer server.Close()
originalHealthEndpoints := append([]string{}, deploymentExternalHealthEndpoints...)
deploymentExternalHealthEndpoints = []string{server.URL + "/healthz", server.URL + "/chat"}
defer func() {
deploymentExternalHealthEndpoints = originalHealthEndpoints
}()
plan.PlanBindingHash = buildPlanBindingFromInputs(
plan.Inputs,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
planPath := filepath.Join(fakeDockerDir, "ready-plan.json")
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, payload, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FAKE_DOCKER_REVISION", plan.ArtifactRevision)
if err := deployApply([]string{"-plan", planPath}); err != nil {
t.Fatalf("expected successful apply with simulated docker and passing health checks, got %v", err)
}
}
func TestDeployApplyHealthGateRejectsOnFailedHTTPHealth(t *testing.T) {
originalPath := os.Getenv("PATH")
fakeDockerDir := t.TempDir()
fakeDocker := filepath.Join(fakeDockerDir, "docker.bat")
dockerBatch := "@echo off\n" +
"if \"%~1\"==\"--context\" (\n" +
" if \"%~3\"==\"compose\" exit /B 0\n" +
" if \"%~3\"==\"inspect\" (\n" +
" if \"%~6\"==\"{{index .Config.Labels \\\"org.opencontainers.image.revision\\\"}}\" echo %FAKE_DOCKER_REVISION%\n" +
" if \"%~6\"==\"{{.State.Status}}\" echo running\n" +
" exit /B 0\n" +
" )\n" +
" if \"%~3\"==\"volume\" if \"%~4\"==\"inspect\" exit /B 0\n" +
")\n" +
"exit /B 1\n"
if err := os.WriteFile(fakeDocker, []byte(dockerBatch), 0o600); err != nil {
t.Fatal(err)
}
os.Setenv("PATH", fakeDockerDir+";"+originalPath)
defer os.Setenv("PATH", originalPath)
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(fakeDockerDir, "docker-compose.prod.yml")
target.ComposeFile = composeFile
if err := os.WriteFile(composeFile, []byte("services:\\n app:\\n image: alpine\\n environment:\\n - POSTGRES_SUPERUSER=${POSTGRES_SUPERUSER}\\n"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(fakeDockerDir, "production.env")
env := []string{
"POSTGRES_SUPERUSER=deploy-apply-test",
"POSTGRES_SUPERUSER_PASSWORD=pw-super",
"CONTINUITY_DB_PASSWORD=pw-cont",
"WORK_DB_PASSWORD=pw-work",
"CAPABILITY_DB_PASSWORD=pw-cap",
"COMMIT_DB_PASSWORD=pw-commit",
"AUTHORITY_DB_PASSWORD=pw-auth",
"IDENTITY_DB_PASSWORD=pw-id",
"SITUATION_DB_PASSWORD=pw-sit",
"CREATOR_USERNAME=creator",
"CREATOR_PASSWORD_FILE=creator-password",
"CREATOR_BOOTSTRAP_TOKEN=creator-bootstrap",
"CREATOR_SESSION_SIGNING_KEY=creator-session",
"COMMIT_EXECUTOR_TOKEN=commit-exec",
"EPISTEMICS_POSTGRES_CREDENTIAL_REF=epi-cred-ref",
"INTERACTION_POSTGRES_CREDENTIAL_REF=int-cred-ref",
"EPISTEMICS_POSTGRES_PASSWORD=epi-password",
"INTERACTION_POSTGRES_PASSWORD=int-password",
}
if err := os.WriteFile(envFile, []byte(strings.Join(env, "\n")), 0o600); err != nil {
t.Fatal(err)
}
knownGood, _, err := captureKnownGood(target, "")
if err != nil || strings.TrimSpace(knownGood) == "" {
knownGood = "11a6a23f6e4f44dd88265a04846419a27f54df98"
}
plan, err := buildDeploymentPlan(root, target, envFile, knownGood, 24, "")
if err != nil {
t.Fatal(err)
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
plan.Inputs,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
planPath := filepath.Join(fakeDockerDir, "ready-plan-bad-health.json")
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, payload, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FAKE_DOCKER_REVISION", plan.ArtifactRevision)
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/healthz":
writer.WriteHeader(http.StatusInternalServerError)
default:
writer.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
originalHealthEndpoints := append([]string{}, deploymentExternalHealthEndpoints...)
deploymentExternalHealthEndpoints = []string{server.URL + "/healthz", server.URL + "/chat"}
defer func() {
deploymentExternalHealthEndpoints = originalHealthEndpoints
}()
if err := deployApply([]string{"-plan", planPath}); err == nil {
t.Fatalf("expected apply to fail when health check fails")
}
}
+3 -1
View File
@@ -56,6 +56,8 @@ func main() {
err = preflight(os.Args[2:])
case "check-diff":
err = checkDiff(os.Args[2:])
case "deploy":
err = deploy(os.Args[2:])
default:
usage()
os.Exit(2)
@@ -67,7 +69,7 @@ func main() {
}
func usage() {
fmt.Println("devctl doctor | check-architecture | show-registry | preflight (-active <json> | -task <json>) | check-diff [-active <json> | -task <json>]")
fmt.Println("devctl doctor | check-architecture | show-registry | preflight (-active <json> | -task <json>) | check-diff [-active <json> | -task <json>] | deploy <prepare|apply> ...")
}
func loadRegistry() (Registry, error) {
+48
View File
@@ -97,6 +97,54 @@ type ProviderUsage struct {
OutputUnits int64
}
type InterpretationApplicability string
const (
InterpretationApplicable InterpretationApplicability = "APPLICABLE"
InterpretationLowConfidence InterpretationApplicability = "LOW_CONFIDENCE"
InterpretationMixedIntent InterpretationApplicability = "MIXED_INTENT"
InterpretationNeedsBroader InterpretationApplicability = "NEEDS_BROADER_INTERPRETATION"
InterpretationClarificationRequired InterpretationApplicability = "CLARIFICATION_REQUIRED"
)
type GatewayInterpretationRequest struct {
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Locale string
MaxOutputBytes int
DataPolicy capability.DataPolicy
DeterministicSpeechAct string
DeterministicSecondary string
DeterministicConfidence int64
DeterministicApplicability InterpretationApplicability
RequestedOutcome string
ProjectNames []string
}
type GatewayInterpretationCandidate struct {
PrimarySpeechAct string `json:"primary_speech_act"`
SecondarySpeechAct string `json:"secondary_speech_act,omitempty"`
ConfidenceMillis int64 `json:"confidence_millis"`
Applicability InterpretationApplicability `json:"applicability"`
RequestedOutcome string `json:"requested_outcome,omitempty"`
NeedsCognition bool `json:"needs_cognition"`
Clarification string `json:"clarification"`
SelectionReason string `json:"selection_reason,omitempty"`
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
}
type GatewayInterpretationResponse struct {
Version ContractVersion
RequestID RequestID
Candidate GatewayInterpretationCandidate
FinishReason FinishReason
Usage *ProviderUsage
}
type AdvisoryResponse struct {
Version ContractVersion
RequestID RequestID
+64 -29
View File
@@ -17,40 +17,54 @@ type ConversationState string
type InterpretationState string
type InteractionRelationKind string
type InterpretationID string
type SemanticCandidateID string
type ProjectReferenceID string
type ProjectReferenceRole string
type ClarificationState string
type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
)
type Conversation struct {
@@ -94,14 +108,35 @@ type UtteranceInterpretation struct {
ID InterpretationID
UtteranceID HumanUtteranceID
SpeechAct SpeechAct
SecondarySpeechAct SpeechAct
ConfidenceMillis int64
Source string
AssertionVerification string
RequestedOutcome string
Clarification ClarificationState
NeedsCognition bool
Applicability SemanticApplicability
SelectionReason string
RecordedAtUnixMillis int64
}
type SemanticCandidate struct {
ID SemanticCandidateID
UtteranceID HumanUtteranceID
Source SemanticCandidateSource
SelectionState SemanticCandidateSelectionState
PrimarySpeechAct SpeechAct
SecondarySpeechAct SpeechAct
ConfidenceMillis int64
Applicability SemanticApplicability
RequestedOutcome string
Clarification ClarificationState
NeedsCognition bool
SelectionReason string
CapabilityHints []string
RecordedAtUnixMillis int64
}
type ProjectReference struct {
ID ProjectReferenceID
UtteranceID HumanUtteranceID
+4
View File
@@ -300,7 +300,11 @@ type PlanningInput struct {
ProjectProvenance ProjectProvenanceRef
Message string
SpeechAct SpeechActRef
SecondarySpeechAct SpeechActRef
CapabilityHints []capability.CapabilityKind
AssertionVerification epistemics.VerificationState
NeedsCognition bool
RequiresClarification bool
ReceivedAtUnixMillis int64
}
@@ -0,0 +1,54 @@
BEGIN;
SET ROLE interaction_role;
ALTER TABLE interaction.utterance_interpretations
ADD COLUMN IF NOT EXISTS secondary_speech_act TEXT NOT NULL DEFAULT 'UNKNOWN',
ADD COLUMN IF NOT EXISTS needs_cognition BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS applicability TEXT NOT NULL DEFAULT 'APPLICABLE',
ADD COLUMN IF NOT EXISTS selection_reason TEXT NOT NULL DEFAULT '';
ALTER TABLE interaction.utterance_interpretations DROP CONSTRAINT IF EXISTS utterance_interpretations_secondary_speech_act_check;
ALTER TABLE interaction.utterance_interpretations
ADD CONSTRAINT utterance_interpretations_secondary_speech_act_check
CHECK (secondary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN'));
ALTER TABLE interaction.utterance_interpretations DROP CONSTRAINT IF EXISTS utterance_interpretations_applicability_check;
ALTER TABLE interaction.utterance_interpretations
ADD CONSTRAINT utterance_interpretations_applicability_check
CHECK (applicability IN ('APPLICABLE','LOW_CONFIDENCE','MIXED_INTENT','NEEDS_BROADER_INTERPRETATION','CLARIFICATION_REQUIRED'));
CREATE TABLE IF NOT EXISTS interaction.semantic_candidates (
candidate_id TEXT PRIMARY KEY,
utterance_id TEXT NOT NULL,
source TEXT NOT NULL,
selection_state TEXT NOT NULL,
primary_speech_act TEXT NOT NULL,
secondary_speech_act TEXT NOT NULL,
confidence_millis BIGINT NOT NULL,
applicability TEXT NOT NULL,
requested_outcome TEXT NOT NULL DEFAULT '',
clarification TEXT NOT NULL,
needs_cognition BOOLEAN NOT NULL DEFAULT FALSE,
selection_reason TEXT NOT NULL DEFAULT '',
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (source IN ('DETERMINISTIC_V2','COGNITIVE_V1')),
CHECK (selection_state IN ('SELECTED','OBSERVED','REJECTED')),
CHECK (primary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
CHECK (secondary_speech_act IN ('DIRECTIVE','ASSERTION','DECISION','PREFERENCE','QUESTION','HYPOTHESIS','CORRECTION','REQUEST_TO_INVESTIGATE','CONFIRMATION','REJECTION','CANCELLATION','CONVERSATIONAL','UNKNOWN')),
CHECK (confidence_millis BETWEEN 0 AND 1000),
CHECK (applicability IN ('APPLICABLE','LOW_CONFIDENCE','MIXED_INTENT','NEEDS_BROADER_INTERPRETATION','CLARIFICATION_REQUIRED')),
CHECK (clarification IN ('NOT_REQUIRED','REQUIRED'))
);
CREATE TABLE IF NOT EXISTS interaction.semantic_candidate_capability_hints (
candidate_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
PRIMARY KEY (candidate_id, capability_kind),
CHECK (candidate_id <> ''),
CHECK (capability_kind <> '')
);
RESET ROLE;
COMMIT;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000071-cognitive-path-reality-generalization.json",
"task_id": "DEV-000071",
"task_file": "dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json",
"task_id": "DEV-000073",
"task_revision": 1
}
@@ -0,0 +1,159 @@
{
"task_id": "DEV-000072",
"task_revision": 1,
"architecture_revision": "DEV-000071-baseline",
"base_repo_revision": "64870798b9a50f895c99d24441703f366df5c98a",
"risk_tier": "E",
"intent": "Implement semantic interpretation and deferral boundary V1 so deterministic interpretation acts as a fast-path candidate rather than unquestionable final truth; preserve provenance between deterministic and cognitive candidates; support mixed intent and safer deferral without activating new providers.",
"non_goal_scope": [
"Do not build a dedicated naming subsystem in this task (for example, no assistant_name table/field).",
"Do not implement dedicated favorite_color/disliked_food/project_role-like preference schemas in this task.",
"Do not introduce a hardcoded 'запомни' keyword trigger or naming-specific memory trigger.",
"Do not introduce dedicated naming memory, universal ontology, or graph/knowledge DB in this task.",
"Treat naming/name-preferral utterances as generic semantic interpretation cases and test fixtures, not the target for persistence architecture changes.",
"Any future durable memory/knowledge mechanism is out of scope for this task and must be captured as a follow-up requirement."
],
"requirement_ids": [
"DEV-000071 measured baseline",
"Deterministic candidate can decline/defer",
"Mixed intent representation",
"Cognition may challenge uncertain deterministic candidate",
"No provider hardcoding",
"No Ollama activation",
"Known and holdout evaluation comparison",
"Production chat acceptance"
],
"allowed_change_scope": {
"modules": [
"interaction",
"work",
"capability",
"epistemics"
],
"paths": [
"contracts/interaction/**",
"contracts/work/**",
"contracts/cognitive/**",
"modules/interaction/**",
"modules/work/**",
"adapters/postgres/interaction_repository.go",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"cmd/cognitive-worker/**",
"database/migrations/**",
"tests/**",
"docs/PROJECT_STATUS.md",
"docs/evidence/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"architecture/adrs/**",
"security/**",
"modules/authority/**",
"modules/commit/**",
"deploy/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": [
"contracts.identity",
"contracts.interaction",
"contracts.work",
"contracts.capability",
"contracts.cognitive",
"contracts.epistemics",
"contracts.situation"
],
"may_change": [
"contracts.interaction",
"contracts.work",
"contracts.cognitive"
]
},
"semantic_owner": "interaction",
"canonical_state": [
"Interaction-owned human utterance interpretation semantics",
"Interaction-owned interpretation provenance between deterministic and cognitive candidates",
"Owner-local persistence expansion for semantic candidate provenance",
"Existing Work planning/help-path semantics updated to consume richer interaction interpretation without changing canonical ownership"
],
"state_transitions": [
"human utterance -> deterministic semantic candidate",
"candidate accepted or deferred",
"deferred candidate -> cognitive candidate",
"candidate selection -> persisted selected interpretation + provenance",
"mixed/consequential ambiguity -> clarification or bounded safe response"
],
"side_effects_and_privileges": [
"No direct external mutation",
"Cognitive Gateway only through existing provider-neutral runtime",
"No direct CPU endpoint calls from chat path",
"No provider activation changes"
],
"persistence_and_migrations": [
"Interaction owner-local migration expected",
"No cross-owner SQL writes",
"No cross-owner FK introduction",
"No new DB owner roles"
],
"failure_retry_restart": [
"If cognition is unavailable, obvious deterministic paths continue to work",
"Uncertain consequential intent must fail closed via clarification or explicit limitation",
"Model output must remain typed candidate evidence, never direct canonical fact/action"
],
"assumptions": [
"Current production has only one proven eligible cognitive binding for human chat: CPU CANARY",
"Ollama/GPU remains inactive for routing in this milestone",
"DEV-000071 artifacts remain the baseline comparison set"
],
"acceptance_scenarios": [
"ask-model request no longer finalized as assertion by default",
"think/investigate wording can defer to cognition without lexical patching as the sole strategy",
"mixed investigate+modify retains both semantic needs",
"correction wording plus new directive preserves both aspects",
"uncertain consequential request defers safely",
"suite_v1 plus DEV-000071 development/holdout comparisons are reported honestly",
"production /chat acceptance covers greeting, cognition question, named implementation request, mixed work, correction, correction+new directive, consequential ambiguity"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"go run ./cmd/devctl check-architecture",
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
"interaction domain tests",
"work planning tests",
"reality evaluation comparisons",
"production /chat acceptance"
],
"future_scope_requirements": [
"Generic interaction-to-memory pipeline (common conceptual flow): interaction -> understood meaning -> potential memory/knowledge candidate -> subject/scope/provenance -> salience/relevance -> persistence decision -> relations/associations -> retrieval -> correction/consolidation.",
"Support multiple ordinary human preference/intention domains (names, preferred forms of address, colors, dislikes, habits, relationships, project facts, machine characteristics, temporary constraints) without special schema-per-example.",
"Use existing Epistemics/Experience/Identity/Situation foundations first; introduce new canonical concepts only when requirements and architecture review justify them."
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,72 @@
{
"task_id": "DEV-000073",
"task_revision": 1,
"architecture_revision": "DEV-000072-baseline",
"base_repo_revision": "36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6b",
"risk_tier": "E",
"intent": "Introduce deterministic two-phase production deployment (prepare/apply) and stale/health/state gates in devctl without changing runtime semantics, so missing required production deployment inputs can never trigger an unsafe recreate.",
"requirement_ids": [
"prepare/apply separation",
"fail-closed required input resolution",
"exact artifact pinning",
"stale plan rejection",
"known-good/rollback capture",
"health verification on apply",
"no production mutation from ambient shell"
],
"allowed_change_scope": {
"paths": [
"AGENTS.md",
"cmd/devctl/**",
"docs/DEPLOYMENT.md",
"docs/PROJECT_STATUS.md",
"docs/evidence/deployment-continuity-dev000073.md",
"architecture/UNRESOLVED_QUESTIONS.md",
"dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json",
"docs/evidence/production-recovery-incident-20260818.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json"
]
},
"forbidden_change_scope": {
"paths": []
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0,
"new_database_owner_roles": 0
},
"semantic_owner": "devctl",
"contracts": {
"consume": [
"contracts.identity"
],
"may_change": []
},
"acceptance_scenarios": [
"fresh shell prepare without ambient production env fails unless required values are in --env-file",
"deploy apply refuses when plan is stale due input/config/revision changes",
"deploy prepare can create READY and BLOCKED plans with machine-readable reasons",
"deploy apply succeeds only for READY plans and verifies deployment artifacts and /health endpoints",
"known-good revision is captured deterministically from production manifest or explicit recovery snapshot",
"healthy production is preserved when PREPARE or APPLY refuse to proceed"
],
"required_tests": [
"go test ./cmd/devctl",
"go run ./cmd/devctl preflight -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json",
"go run ./cmd/devctl check-diff -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json",
"go run ./cmd/devctl deploy prepare --target production --compose deploy/docker-compose.prod.yml --env-file <production-bootstrap.env> --plan .devctl/deployment-plans/test-dev073.json",
"go run ./cmd/devctl deploy apply --plan .devctl/deployment-plans/test-dev073.json --dry-run",
"go run ./cmd/devctl deploy apply --plan .devctl/deployment-plans/missing.json"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
+25
View File
@@ -23,6 +23,31 @@ powershell -ExecutionPolicy Bypass -File .\deploy\windows\Confirm-DomovoyProduct
The confirmation script accepts the promotion only when every project-built container has that revision label, then atomically records its exact Docker image ID in `%LOCALAPPDATA%\Domovoy\monitoring\deployment-manifest.json` and the private NAS monitoring directory. Production health compares every running label and image ID to this manifest and becomes degraded on drift. The manifest is non-secret operational evidence; it is not canonical Assistant state.
## Production deploy pipeline V1
`cmd/devctl` is now the normal production deployment entrypoint and uses two explicit stages:
- `deploy prepare` reads the compose manifest and an explicit durable bootstrap source (for production: `--env-file`) and resolves required inputs.
- `deploy apply` consumes a prepared deployment plan and is the only normal mutation path.
Safety properties:
- Missing/empty/unresolvable required input blocks prepare readiness (`BLOCKED`) and blocks apply.
- Ambient environment is not authoritative for required production inputs.
- A prepare includes the resolved source revision, config revision, secret references, resolved input classification, migration readiness, known-good snapshot (if available), and a materialized plan binding hash.
- `deploy apply` refuses to execute stale plans (expired, mismatched compose, changed source/config, input drift).
- `apply` verifies running containers, required volumes, and external health (`/healthz`, `/chat`) before returning ready.
- Rollback path to recorded known-good is prepared for recovery when explicitly allowed (`--auto-rollback`).
Operational sequence example:
```powershell
go run ./cmd/devctl deploy prepare --target production --compose deploy/docker-compose.prod.yml --env-file <fresh-production.env> --plan .devctl/deployment-plans/prod-<rev>.json
go run ./cmd/devctl deploy apply --plan .devctl/deployment-plans/prod-<rev>.json
```
`prepare`/`apply` are intentionally decoupled so a fresh Codex session with only repository checkout and durable sources can complete production deployment safely.
The first external operation adds `assistant-notification-receiver-1` on production host port `18081`. Caddy applies `deploy/caddy/assistent.caddy`, routing only `https://assistent.cin.su/delivery/*` to that port and all other paths to Creator Web on `18080`. The receiver has an isolated durable volume and no Assistant database access. Commit success means an immutable receiver receipt exists; Windows display and human reading are not Commit outcomes.
Migrations `010_capability_notification_binding.sql` and `011_commit_notification_envelope.sql` must be applied before the new runtimes start. The initial Capability binding is owner-local for `domovoy-creator-principal`, revision 1, capability `prospective-notification-delivery`, origin `https://assistent.cin.su`, delivery path `/delivery/v1/deliveries`, receipt path `/delivery/v1/receipts`, protocol `IDEMPOTENT_RECEIPT_V1`, and secret reference `domovoy-notification-write-v1`.
+70 -1
View File
@@ -1,6 +1,75 @@
# Assistant project status
Audit date: 2026-08-17. Active implementation: DEV-000071 revision 1.
Audit date: 2026-08-18. Active implementation: DEV-000072 revision 1.
## Production recovery checkpoint (2026-08-18)
- `PRODUCTION_RECOVERY_STATUS = RESTORED_TO_KNOWN_GOOD`
- `TARGET_REVISION = 11a6a23f6e4f44dd88265a04846419a27f54df98`
- `PREVIOUS_RUNNING_REVISION = 36fec6c4f8aabf5e9f5f4ac0d5e2f9c0b8a5a9f0`
- `CURRENT_RUNNING_REVISION = 11a6a23f6e4f44dd88265a04846419a27f54df98`
- `PRODUCTION_HEALTH = healthy`
- `POSTGRES_HEALTH = healthy`
- `CORE_RUNTIME_HEALTH = healthy`
- `CONTINUITY_RUNTIME_HEALTH = healthy`
- `COMMIT_RUNTIME_HEALTH = healthy`
- `CREATOR_WEB_HEALTH = healthy`
- `EXTERNAL_HEALTHZ = 200`
- `CHAT_UI = 200`
- `INCIDENT_EVIDENCE_PATH = docs/evidence/production-recovery-incident-20260818.md`
- `DEV_000072_STATUS = PRODUCTION_ACCEPTANCE_BLOCKED_BY_DEPLOYMENT_INCIDENT`
- `NEXT_REQUIRED_ACTION = run post-recovery evidence tasks for DEV-000072 acceptance and then continue with production deployment`
## DEV-000073 checkpoint
- `DEV_000073_STATUS = BLOCKED`
- `STATUS = PAUSED_PENDING_LONG_HORIZON_MODEL`
- `CURRENT_PHASE = DURABLE_BOOTSTRAP_PARTIAL_IMPLEMENTATION_STABILIZED`
- `DEV_000073_SCOPE = production deployment continuity + safe redeploy V1 (deploy prepare/apply)`
- `START_DATE = 2026-08-18`
- `ACTIVE_TASK = dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json`
- `LAST_VERIFIED_COMMIT = 36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6b`
- `CURRENT_UNCOMMITTED_WORK = cmd/devctl/deploy.go (untracked in git index); cmd/devctl/deploy_test.go (untracked in git index); docs/evidence/deployment-continuity-dev000073.md (untracked in git index); supporting task/evidence files`
- `DEV_000073_EVIDENCE_PATH = docs/evidence/deployment-continuity-dev000073.md`
- `NEXT_ACTION = resume DEV-000073 from this checkpoint using a longer-horizon model to complete durable bootstrap review and production-safe seed plan`
- `BLOCKER = real sanitized production prepare cannot resolve all required inputs from intentional durable/protected sources`
- `GO_TEST_DEVCTL = EXECUTED_PASS`
- `CORE_OUTPUTS = cmd/devctl/deploy.go, cmd/devctl/deploy_test.go`
- `DOC_OUTPUTS = AGENTS.md, docs/DEPLOYMENT.md, docs/PROJECT_STATUS.md`
- `DEPLOYMENT_RULE = production mutation requires explicit prepared plan and fail-closed unresolved required input checks`
- `SEMANTIC_SAFETY = no runtime behavior change; no provider naming/memory changes; DEV-000072 remains unchanged and not promoted`
- `TEMP_ARTIFACT_CLEANUP = cmd/devctl/__tmp_devctl_patch_test.txt absent`
- `BLOCKERS = final durable bootstrap design/implementation review remains; pre-runtime protected Secret Resolver bridge and one-time production seed are not yet completed`
- `IMPORTANT_FINDINGS = deploy prepare/apply gates and negative path protections are functionally exercised; production continuity still depends on durable input source for real use`
- `EVIDENCE_OBTAINED = docs/evidence/deployment-continuity-dev000073.md`
- `COMPLETED = deployment prepare/apply safety mechanics; missing input fail-closed; stale-plan rejection; exact-artifact binding; secret-leak protection; health gate; migration discovery/precondition; seven-input ownership audit; canonical-vs-derived credential decision; current deploy.go partial bootstrap implementation compiles; go test ./cmd/devctl passes`
- `ARCHITECTURAL_DECISIONS_ALREADY_ACCEPTED = do not repeat seven-input ownership audit; DB CredentialRef is canonical; raw DB password is derived deployment/runtime transport; CREATOR_PASSWORD_FILE is runtime/mount transport; creator bootstrap token and session signing key require durable protected ownership`
- `IN_PROGRESS = final durable bootstrap design/implementation review; pre-runtime protected secret resolver bridge; one-time production canonical seed/migration; real fresh sanitized production prepare = READY`
- `REASON_FOR_PAUSE = available model is optimized for short interactive edits; remaining work requires long-horizon architecture + cross-component reasoning and production-state migration`
- `NEXT_REQUIRED_ACTION = resume DEV-000073 from this checkpoint using a stronger long-horizon Codex model`
## DEV-000072 checkpoint
- `DEV_000072_STATUS = LOCAL_ACCEPTANCE_PASS`
- `LAST_COMPLETED = semantic deferral boundary V1 implementation plus regression/generalization acceptance: deterministic semantic candidates, cognitive interpretation gateway path, owner-local candidate provenance persistence, mixed-intent planning input propagation, punctuation/metamorphic stability fixes, capability-gap/reminder/work-path repair, and live A/B/C trace evidence`
- `ARCHITECTURE_CHANGE = none; interaction owner remains unchanged and no new cross-owner dependency was accepted`
- `SEMANTIC_BOUNDARY_CHANGE = deterministic interpretation no longer acts as unquestionable final truth for low-confidence / broader-interpretation / mixed-intent cases; core runtime can request typed cognitive reinterpretation through the existing provider-neutral gateway`
- `PERSISTENCE_CHANGE = migration 027 adds secondary speech act / applicability / selection reason on interaction interpretations plus semantic candidate provenance tables`
- `GIT_ANCESTRY_STATUS = PASS: current branch merge-base equals origin/main at 64870798b9a50f895c99d24441703f366df5c98a; DEV-000071 commit c1aff60443829892ba70104f40b55de01f186a7f is contained in origin/main`
- `CHECK_DIFF_STATUS = PASS: diff scope checks OK for DEV-000072 against base 64870798b9a50f895c99d24441703f366df5c98a`
- `KNOWN_BASELINE_COMPARISON = suite_v1 baseline failed_available 4 / intent 0 / capability 3 / consequence 1 -> current failed_available 0 / intent 0 / capability 0 / consequence 0`
- `HOLDOUT_AND_GENERALIZATION_COMPARISON = suite_v2 baseline failed_available 18 / intent 3 / capability 7 / consequence 4 / unknown 7 -> current failed_available 0 / intent 0 / capability 0 / consequence 0 / unknown 0`
- `UNNECESSARY_COGNITION = no observed increase in suite_v1 or suite_v2 current-run`
- `UNNECESSARY_CLARIFICATION = current replay counts remain observable (suite_v1 1, suite_v2 5) and are recorded explicitly for operator review rather than hidden by aggregate PASS`
- `LIVE_TRACE_EVIDENCE = PASS: raw CPU Qwen understood "ты кто" and punctuation-free modal request; Domovoy semantic path now routes "ты кто" and "можешь расказать об утре" into cognition-worthy interpretation; gateway-to-same-CPU trace preserved question semantics`
- `METAMORPHIC_EVIDENCE = docs/evidence/semantic-deferral-metamorphic-evidence-2026-08-17-v1.json`
- `RAW_MODEL_BOUNDARY = recorded explicitly: raw cognitive models identify themselves as provider/model provenance; this is not patched inside Interaction and remains a later context-compilation milestone`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; ASSISTANT_LIVE_SEMANTIC_TRACE=1 go test ./cmd/cognitive-worker -run TestLiveInterpretationTrace -v`
- `COGNITIVE_CONTEXT_GAP = still open by design in this milestone: raw providers identify themselves as Qwen/Gemma provenance when Domovoy identity/context is not compiled into the prompt`
- `NON_GOAL_SCOPE_DEV_000072 = naming as memory domain is a semantic test case only in this task; no assistant_name subsystem, no favorite_color fields, no naming-specific memory table/schema, no explicit 'запомни'-only trigger, no universal ontology, and no graph DB are introduced for this deployment boundary`
- `GENERAL_MEMORY_REQUIREMENT = broader generic knowledge-retention admission (for preferences, names, constraints, relations, project facts, recurring patterns, and analogous domains) is out-of-scope for DEV-000072 and is recorded as UQ-016 in architecture/UNRESOLVED_QUESTIONS.md`
- `PRODUCTION_STATUS = pending promotion/deploy from this checkpoint`
- `NEXT_RECOMMENDED_STEP = finish DEV-000072 production promotion, apply migration 027, redeploy runtime, verify live /chat against the promoted artifact, then address broader memory/knowledge retention via a separate evidence-backed milestone`
## DEV-000071 checkpoint
@@ -0,0 +1,169 @@
# DEV-000073 Deployment Continuity Evidence (Checkpoint)
Audit date: 2026-08-18
Task ID: `DEV-000073`
Task revision: `1`
Base revision: `36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6c`
## Status summary
- `DEV_000073_STATUS = BLOCKED`
- `CURRENT_PHASE = DURABLE_BOOTSTRAP_PARTIAL_IMPLEMENTATION_STABILIZED`
- `CHECKPOINT_STATUS = IN_PROGRESS`
- `LAST_VERIFIED_COMMIT = 36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6b`
- `LAST_VERIFIED_TEST_COMMAND = go test ./cmd/devctl`
- `LAST_VERIFIED_TEST_RESULT = EXECUTED_PASS`
- `CURRENT_UNCOMMITTED_DIFF = cmd/devctl/deploy.go, cmd/devctl/deploy_test.go, docs/DEPLOYMENT.md, AGENTS.md, architecture/UNRESOLVED_QUESTIONS.md, dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json, docs/evidence/deployment-continuity-dev000073.md, docs/evidence/production-recovery-incident-20260818.md, docs/PROJECT_STATUS.md, dev/ACTIVE_TASK.json`
- `CURRENT_TASK_STATE = PAUSED_PENDING_LONG_HORIZON_MODEL`
## Executed checks
- `go test ./cmd/devctl` → `EXECUTED_PASS`
- `go run ./cmd/devctl preflight -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json` → `EXECUTED_PASS`
- `go run ./cmd/devctl check-diff -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json` → `EXECUTED_PASS`
- `go test ./cmd/devctl` targeted acceptance set (`TestDeployPrepareBlockedAndApplyRefusesMissingRequiredInput`, `TestAssertPlanCurrentRejectsStaleInputChange`, `TestAssertPlanCurrentRejectsArtifactMismatch`, `TestDeploymentPlanDoesNotLeakSecretValues`, `TestDeployApplyRunsHealthGateWithReadyPlanAndSimulatedComposeDocker`, `TestDeployApplyHealthGateRejectsOnFailedHTTPHealth`, migration precondition tests) → `EXECUTED_PASS`
## Task-specific acceptance probes
- `MISSING_INPUT_NEGATIVE_TEST = EXECUTED_PASS`
- missing required input in prepare → `readiness=BLOCKED`
- `deploy apply` with blocked plan returns non-zero
- `STALE_PLAN_NEGATIVE_TEST = EXECUTED_PASS`
- compose/input/content assumption changes in plan → `assertPlanCurrent` detects stale state
- `EXACT_ARTIFACT_NEGATIVE_TEST = EXECUTED_PASS`
- artifact change without plan refresh → `assertPlanCurrent` detects stale mismatch
- `SECRET_LEAK_TEST = EXECUTED_PASS`
- plan output and tests show no plaintext secret values; only source class and hashes persisted
- `HEALTH_GATE_TEST = EXECUTED_PASS`
- docker compose success + passing HTTP checks + docker inspect => apply may complete
- docker compose success + failed `/healthz` => apply fails
## Production continuity probe (sanitized shell)
- `PRODUCTION_REVISION_VERIFIED = 11a6a23f6e4f44dd88265a04846419a27f54df98`
- `REAL_PRODUCTION_PREPARE_READINESS = BLOCKED`
- `REAL_PRODUCTION_SANITIZED_PREPARE = NOT_READY`
- `REAL_PRODUCTION_CONFIG_SOURCE = TEMPORARY_SYNTHETIC`
- `DOCKER_PROD_BOOTSTRAP_ENV_SOURCE = secrets/docker-prod-bootstrap.env`
- `LEGACY_AMBIENT_DEPENDENCIES = none`
- `UNKNOWN_INPUTS = none`
- `KNOWN_INPUT_SOURCE_DETAIL` for required fields:
- `ASSISTANT_SOURCE_REVISION = missing`
- `CREATOR_WEB_PORT = missing`
- `NOTIFICATION_RECEIVER_PORT = missing`
- `POSTGRES_DB = file`
- all resolved/required production inputs include:
- resolved from file: `POSTGRES_SUPERUSER`, `POSTGRES_SUPERUSER_PASSWORD`, `CONTINUITY_DB_PASSWORD`, `WORK_DB_PASSWORD`, `CAPABILITY_DB_PASSWORD`, `COMMIT_DB_PASSWORD`, `AUTHORITY_DB_PASSWORD`, `IDENTITY_DB_PASSWORD`, `SITUATION_DB_PASSWORD`, `CREATOR_USERNAME`, `COMMIT_EXECUTOR_TOKEN`
- missing (blocking): `CREATOR_BOOTSTRAP_TOKEN`, `CREATOR_PASSWORD_FILE`, `CREATOR_SESSION_SIGNING_KEY`, `EPISTEMICS_POSTGRES_CREDENTIAL_REF`, `EPISTEMICS_POSTGRES_PASSWORD`, `INTERACTION_POSTGRES_CREDENTIAL_REF`, `INTERACTION_POSTGRES_PASSWORD`
## Configuration source classification (required production inputs)
- `POSTGRES_SUPERUSER -> BOOTSTRAP` (from env file path)
- `POSTGRES_SUPERUSER_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `CONTINUITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `WORK_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `CAPABILITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `COMMIT_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `AUTHORITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `IDENTITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `SITUATION_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `CREATOR_USERNAME -> BOOTSTRAP` (from env file path)
- `CREATOR_PASSWORD_FILE -> DOMOVOY_MANAGED` (missing)
- `CREATOR_BOOTSTRAP_TOKEN -> SECRET_REFERENCE` (missing)
- `CREATOR_SESSION_SIGNING_KEY -> SECRET_REFERENCE` (missing)
- `COMMIT_EXECUTOR_TOKEN -> SECRET_REFERENCE` (from env file path)
- `EPISTEMICS_POSTGRES_CREDENTIAL_REF -> SECRET_REFERENCE` (missing)
- `INTERACTION_POSTGRES_CREDENTIAL_REF -> SECRET_REFERENCE` (missing)
- `EPISTEMICS_POSTGRES_PASSWORD -> SECRET_REFERENCE` (missing)
- `INTERACTION_POSTGRES_PASSWORD -> SECRET_REFERENCE` (missing)
- `CONFIG_SOURCE_CLASSIFICATION = TEMPORARY_SYNTHETIC`
## Migration readiness
- `CURRENT_MIGRATION_STATE_SOURCE = git ls-tree -r --name-only <revision> database/migrations`
- `CANDIDATE_MIGRATION_REQUIREMENT = current_max=27 target_max=27 status=matched need_known_good=false (for plan created from 11a6a23f6e4f...)`
- `MIGRATION_DISCOVERY_STATUS = PASS`
- `PRECONDITION_ENFORCEMENT = PASS`
- `MIGRATION_PRECONDITION_STATUS = PASS` (plan execution enforces migration precondition and rejects upgrade-without-known-good as blocking)
- `ROLLBACK_COMPATIBILITY_STATUS = not reached by this evidence run; migration check is enforced before compose mutation`
## Gaps / blockers
- `BLOCKER = REAL_PRODUCTION_SANITIZED_PREPARE depends on `secrets/docker-prod-bootstrap.env` and still has 7 required missing inputs`
- `BLOCKER = production bootstrap source is temporary/synthetic in this environment; no durable/protected production bootstrap source has been established here`
- `REASON_FOR_PAUSE = remaining work requires long-horizon model for durable bootstrap review, resolver bridge completion, and production seed migration planning`
## Evidence persisted
- `evidence/evidence file: docs/evidence/deployment-continuity-dev000073.md`
- `checkpoint: docs/PROJECT_STATUS.md`
## Durable Ownership Audit (DEV-000073)
INPUT_OWNERSHIP_MATRIX
| INPUT_NAME | WHY_NEEDED | SEMANTIC_OWNER | CANONICAL_STATE | SECRET | INTENDED_DURABLE_SOURCE | CURRENT_ACTUAL_SOURCE | RESOLUTION_PATH | WHY_CURRENT_PREPARE_CANNOT_RESOLVE_IT |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| CREATOR_BOOTSTRAP_TOKEN | bootstrap token for the Creator domain startup/authentication bootstrap flow | identity | yes | yes | durable secret input for production bootstrap (protected secret value) | missing from sanitized workspace prepare inputs | deploy.prepare file/env resolution only (`inputSourceClass=SECRET_REFERENCE`) | no durable bootstrap record supplied for this token in this environment; prepare does not have a secret backend lookup step |
| CREATOR_PASSWORD_FILE | creator password secret-file locator consumed by core runtime identity startup | identity | no | no | domovoy-managed runtime mount under `/run/secrets` (`core-runtime` creator password path) | provided in compose as a fixed container path but not as resolved prepare input | deploy.prepare accepts `_FILE` as an input class but only from env file/ambient and does not consume compose defaults | compose default is outside deploy.prepare source model, so required input remains unresolved even when container expects the path |
| CREATOR_SESSION_SIGNING_KEY | key for secure creator session signing | identity | yes | yes | durable secret input for production bootstrap (protected secret value) | missing from sanitized workspace prepare inputs | deploy.prepare file/env resolution only (`inputSourceClass=SECRET_REFERENCE`) | no durable bootstrap source in this environment; no value in env file and not provided by resolver bootstrap path |
| EPISTEMICS_POSTGRES_CREDENTIAL_REF | identity of epistemics credential record for DB secret resolution | epistemics | yes | no | protected durable secret-reference declaration for the epistemics DB credential record | missing from sanitized workspace prepare inputs | deploy.prepare expects direct env value (`SECRET_REFERENCE`) | credential reference value is absent and there is no durable bootstrap source for this reference name |
| EPISTEMICS_POSTGRES_PASSWORD | DB password transport input currently listed as required by deploy | epistemics | no | yes | derived at runtime from `EPISTEMICS_POSTGRES_CREDENTIAL_REF` + `EPISTEMICS_POSTGRES_PASSWORD_FILE` through local secret resolver | not present as input in prepare; compose sets file-based runtime path for password value | unresolved by deploy.prepare because it is treated as legacy required input, while runtime reads file-backed path | this value is rendered runtime transport data; deploy.prepare contract has no mapping from `*_CREDENTIAL_REF` to `*_PASSWORD` |
| INTERACTION_POSTGRES_CREDENTIAL_REF | identity of interaction credential record for DB secret resolution | interaction | yes | no | protected durable secret-reference declaration for the interaction DB credential record | missing from sanitized workspace prepare inputs | deploy.prepare expects direct env value (`SECRET_REFERENCE`) | credential reference value is absent and there is no durable bootstrap source for this reference name |
| INTERACTION_POSTGRES_PASSWORD | DB password transport input currently listed as required by deploy | interaction | no | yes | derived at runtime from `INTERACTION_POSTGRES_CREDENTIAL_REF` + `INTERACTION_POSTGRES_PASSWORD_FILE` through local secret resolver | not present as input in prepare; compose sets file-based runtime path for password value | unresolved by deploy.prepare because it is treated as legacy required input, while runtime reads file-backed path | this value is rendered runtime transport data; deploy.prepare contract has no mapping from `*_CREDENTIAL_REF` to `*_PASSWORD` |
CREDENTIAL_REF_PASSWORD_DUPLICATION_ASSESSMENT
`B) CredentialRef is canonical and PASSWORD is derived/rendered runtime transport value.`
Rationale:
- `cmd/core-runtime/main.go` `readCredentialRefSecret(name, refName)` rejects direct `name` secret values (`direct environment secret is forbidden`) and requires `name_FILE` + `<name>_CREDENTIAL_REF`.
- It resolves credentials through `platformsecrets.NewFileResolver` using `SecretFileRef{CredentialRef: ref, Purpose: DatabasePassword}`.
- The compose layout includes `<OWNER>_POSTGRES_CREDENTIAL_REF` and `<OWNER>_POSTGRES_PASSWORD_FILE`, reinforcing the canonical-reference + rendered-file pattern.
CREATOR_INPUT_OWNERSHIP
- `CREATOR_BOOTSTRAP_TOKEN`
- Real owner: `identity`.
- Real durable source class: protected secret (identity bootstrap token), **not** in repo and not present in sanitized prepare input.
- Current classification: missing required input in this environment.
- `CREATOR_SESSION_SIGNING_KEY`
- Real owner: `identity`.
- Real durable source class: protected secret (identity session signing key), **not** in repo and not present in sanitized prepare input.
- Current classification: missing required input in this environment.
- `CREATOR_PASSWORD_FILE`
- Real owner: `identity` (runtime auth material locator).
- It is a **rendered runtime path** backed by Domovoy/runtime secret mount conventions (`/run/secrets`) and is **not** a canonical secret value.
- Current classification: compose-fixed path, not a standalone durable deploy input for this phase.
DOCKER_PROD_BOOTSTRAP_ENV_ROLE
`TEMPORARY_SYNTHETIC`
Why: the file is a local repository bootstrap artifact used for sanitized shell-level prepare/apply probing; it is not an intentional durable/protected production source and still lacks required durable secret inputs. It is neither a Domovoy-managed state artifact nor a recoverable provenance artifact for real production bootstrap.
EXISTING_SECRET_RESOLVER_CAN_SUPPLY_MISSING_INPUTS
`partial`
- adapter? `no` (deploy.prepare does not invoke runtime secret adapter; this check is done in runtime after plan creation)
- bootstrap locator? `missing` (no durable, environment-wide bootstrap secret locator for all 7 inputs is present in this workspace)
- credential record? `partially present in contracts, not provisioned here` (code path expects `*_PASSWORD_FILE` + `*_CREDENTIAL_REF`, but missing refs/records in this environment)
- protected backend entry? `missing` (no proven production-grade protected entry available via the current prepare evidence run)
- ownership mapping? `partial` (owners/areas can be inferred from contracts and module code, but prepare-time ownership policy is not the missing-input resolver)
- pre-runtime bootstrap cycle? `not demonstrated` (no cycle blocker)
BOOTSTRAP_CYCLE_EXISTS
`no`
No real cycle was found because secret resolution uses local file-backed resolver in core runtime and is not a dependency of Domovoy boot for deployment plan creation in this repository evidence path.
MINIMUM_IMPLEMENTATION_GAP
- Provide a durable/protected bootstrap source for the remaining required identity inputs (`CREATOR_BOOTSTRAP_TOKEN`, `CREATOR_SESSION_SIGNING_KEY`, `CREATOR_PASSWORD_FILE`) and epistemics/interaction credential refs.
- Align deploy input contract so production input readiness depends on the canonical pair inputs (`*_CREDENTIAL_REF` + `*_PASSWORD_FILE`) and not legacy `<OWNER>_POSTGRES_PASSWORD` as a required direct input.
- Add evidence-backed mapping for each canonical input’s ownership/source class and bootstrap provisioning path before proceeding to acceptance/ready status.
CHECKPOINT_UPDATED = yes
EVIDENCE_PERSISTED = yes
@@ -0,0 +1,166 @@
[
{
"id": "A",
"status": 200,
"message": "ты кто",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "86fb129b40d2f2fbb4ab798d863bf35d",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Я искусственный интеллект, созданный для помощи. Как я могу быть полезен сейчас?"
},
{
"id": "B",
"status": 200,
"message": "сколько будет 2 + 2",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "570663950030add34f71cedb24f4256d",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "2 + 2 equals 4."
},
{
"id": "C",
"status": 200,
"message": "как с утра",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "008192deff274c8ab7678c180ab7e04d",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Утро - это прекрасное время для начала нового дня. Начните с здорового питания, которое поможет вам начать день с бодрости и энергии. Не забудьте о hydratation, выпив适量的水. Уделите время саморазвитию, например, чтению или медитации, чтобы зарядить позитивом весь день."
},
{
"id": "D",
"status": 200,
"message": "проведи анализ: почему тормозит база, и потом добавь в задачу пометку для проверки",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "4607460cd39d273d99ae50a7d8315517",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "У меня есть несколько возможных ожиданий. Уточните, о каком именно речь: дедлайн в пятницу; дедлайн в пятницу; дедлайн в пятницу"
},
{
"id": "E",
"message": "исправь прошлую формулировку",
"status": 502,
"body": "chat unavailable\n"
},
{
"id": "F",
"message": "обратись к модели gemma4 и спроси сколько звезд на небе",
"status": 502,
"body": "chat unavailable\n"
},
{
"id": "G",
"status": 200,
"message": "просто напомни, что нужно проверить",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "89b69b8251258a689f38de98552cbed4",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."
},
{
"id": "H",
"status": 200,
"message": "Запомни тебя зовут Домовой, я тебя буду звать Домовым",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "0c3bcf9b8f423e93b1b255aa33a557db",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Записал как утверждение человека. Это пока не проверенный факт."
},
{
"id": "H2",
"status": 200,
"message": "для меня ты Домовой",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "7847420f28c85894400c6a5be3e760d0",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Записал как утверждение человека. Это пока не проверенный факт."
},
{
"id": "H3",
"status": 200,
"message": "давай я буду звать тебя Домовым",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "085e4194a61cbfd097118553342e063a",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Записал как утверждение человека. Это пока не проверенный факт."
}
]
@@ -0,0 +1,184 @@
[
{
"id": "A",
"status": 200,
"message": "ты кто",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "7d14f020fc2134f0ba995c35ebe31523",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"reply": "Я искусственный интеллект, созданный для помощи. Как я могу быть полезен сейчас?",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "B",
"status": 200,
"message": "сколько будет 2 + 2",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "0a739b4810420e0eb0615d8d4cd4b60b",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"reply": "2 + 2 equals 4.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "C",
"status": 200,
"message": "как с утра",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "4463c0cc31a552a23c0c0ba4eff06554",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"reply": "Утро - это прекрасное время для начала нового дня. Начните с здорового питания, которое поможет вам начать день с бодрости и энергии. Не забудьте о hydratation, выпив适量的水. Уделите время саморазвитию, например, чтению или медитации, чтобы зарядить позитивом весь день.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "D",
"status": 200,
"message": "проведи анализ: почему тормозит база, и потом добавь в задачу пометку для проверки",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "c80c1eb2ce08edca3add568825c444d7",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"reply": "У меня есть несколько возможных ожиданий. Уточните, о каком именно речь: дедлайн в пятницу; дедлайн в пятницу; дедлайн в пятницу",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "EXPLICIT",
"active_focus": "УПО"
},
{
"id": "E",
"status": 502,
"message": "исправь прошлую формулировку",
"error": "request_failed"
},
{
"id": "F",
"status": 502,
"message": "обратись к модели gemma4 и спроси сколько звезд на небе",
"error": "request_failed"
},
{
"id": "G",
"status": 200,
"message": "просто напомни, что нужно проверить",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "0519382bed09ae5ea9c9a3b2f2af2641",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"reply": "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H",
"status": 200,
"message": "Запомни тебя зовут Домовой",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "aae512074bf81adb405c0c80331c9096",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Записал как утверждение человека. Это пока не проверенный факт.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H2",
"status": 200,
"message": "для меня ты Домовой",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "78a171bb2f405e72f98f3b19d7422514",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Записал как утверждение человека. Это пока не проверенный факт.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H3",
"status": 200,
"message": "давай я буду звать тебя Домовым",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "382bd2310ed4a86f671f02c45afe3aee",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Записал как утверждение человека. Это пока не проверенный факт.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H4",
"status": 200,
"message": "Вернемся к УПО",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "8cb57dcf2599947673d1c1a4bb154266",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Фокус переключён: УПО.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "EXPLICIT",
"active_focus": "УПО"
},
{
"id": "I",
"status": 200,
"message": "помоги ответить на письмо бухгалтеру",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "ebeacce8340779490936e702b67543fa",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения.",
"work": false,
"capability_status": "UNAVAILABLE",
"capability_kind": "SEND_EMAIL",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
}
]
@@ -0,0 +1,82 @@
## Production Recovery Incident Log
Date: 2026-08-18
Objective: Restore last known-good production with minimal changes, no code deploy from DEV-000072.
### 1) Pre-recovery verification (reconfirmed)
- Last observed bad running revision: `36fec6c4f8aabf5e9f5f4ac0d5e2f9c0b8a5a9f0`.
- Last known-good revision from durable manifests: `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- Known-good deployment artifact IDs were present in `docker-prod` and matched both:
- `%LOCALAPPDATA%\Domovoy\monitoring\deployment-manifest.json`
- `\\nas\MST\codex\_monitoring\domovoy\deployment-manifest.json`
- Pre-recovery container state:
- `assistant-postgres-1` was running but unhealthy.
- `assistant-notification-receiver-1` healthy.
- `assistant-core-runtime-1`, `assistant-commit-runtime-1`, `assistant-cognitive-worker-1`, `assistant-creator-web-1` were `Created`.
- `assistant-continuity-runtime-1` was restarting.
- Continuity startup logs repeatedly reported `COMMIT_EXECUTOR_TOKEN is required`.
- DB read checks were reachable through `postgres` role and owner roles existed; schema objects in main owner schemas were present, including `interaction.utterance_interpretations` + `interaction.semantic_candidates` and expected 4 semantic columns.
### 2) Recovered configuration inputs
Classification used:
- `DURABLE_CANONICAL`: long-lived non-secret production evidence.
- `PROTECTED_DURABLE`: host/docker secret artifacts that are persistent, permission-protected inputs.
- `BREAK_GLASS_RUNTIME_EVIDENCE`: local runtime incident recovery metadata used only after verification.
- `POSTGRES_SUPERUSER` / passwords:
- `DURABLE_CANONICAL`: compose contract requires them.
- `PROTECTED_DURABLE`: secret-backed postgres volumes for password material.
- `BREAK_GLASS_RUNTIME_EVIDENCE`: `secrets/production-recovery.env` provided non-empty values and enabled rollback configuration.
- `COMMIT_EXECUTOR_TOKEN`:
- `DURABLE_CANONICAL`: required by continuity/commit contracts.
- `BREAK_GLASS_RUNTIME_EVIDENCE`: populated from `secrets/production-recovery.env`.
- `CREATOR_USERNAME`:
- `BREAK_GLASS_RUNTIME_EVIDENCE`: `secrets/production-recovery.env` (`m`).
- `epistemics_db_password`, `interaction_db_password`, `creator_bootstrap_token`, `creator_password`, `creator_session_signing_key`,
`notification writer/reader tokens`:
- `PROTECTED_DURABLE`: existing Docker volumes `assistant_core_secrets`,
`assistant_notification_writer_secret`, `assistant_notification_reader_secret`.
### 3) Recovery plan (finalized)
- Target revision: `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- Keep durable data volumes unchanged:
- `assistant_assistant_pg`
- `assistant_core_secrets`
- `assistant_notification_writer_secret`
- `assistant_notification_reader_secret`
- `assistant_notification_receipts`
- `assistant_nats`
- Action set:
1. Retag known-good images from local/NAS manifests as active `assistant-* : latest`.
2. Recreate assistant stack on `docker-prod` with `--env-file secrets/production-recovery.env`, `--no-build`, `--force-recreate`.
3. Verify health and endpoint checks listed in section 5.
### 4) Recovery actions executed
- Recreated all assistant services on `docker-prod` (and `nats`) from known-good image IDs.
- Loaded production recovery env (`secrets/production-recovery.env`) into compose execution.
- Confirmed required runtime env is now present in service env:
- continuity/commit `COMMIT_EXECUTOR_TOKEN` is set.
- db user/passwords + creator bootstrap wiring are set.
- Verified all services now run with revision `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- No credential values were printed in logs or evidence text.
### 5) Verification results
- `POSTGRES_HEALTH`: OK (postgres accepts connections).
- `CORE_RUNTIME_HEALTH`: OK (`/healthz` returns runtime ok).
- `CONTINUITY_RUNTIME_HEALTH`: OK (`/healthz` returns runtime ok).
- `COMMIT_RUNTIME_HEALTH`: OK (`/healthz` returns runtime ok).
- `CREATOR_WEB_HEALTH`: OK (`/healthz` returns runtime ok; container healthy).
- `EXTERNAL_HEALTH`: `https://assistant.cin.su/healthz` returns `HTTP 200`.
- `CHAT_SMOKE`: `https://assistant.cin.su/chat` returns `HTTP 200`; authenticated smoke message sent successfully via `POST /api/chat/messages` after session creation (harmless message `health check`), returning `HTTP 200`.
- Production artifact confirmation succeeded for revision `11a6a23f6e4f44dd88265a04846419a27f54df98` with 7 services.
### 6) Final state
- `KNOWN_GOOD_IMAGE_STATUS`: all required known-good image IDs present and active.
- `PRODUCTION_REVISION_AFTER_RECOVERY`: `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- `DATA_INTEGRITY_STATUS`: preserved; existing schemas and `interaction.utterance_interpretations` semantic columns remain present.
- `CREDENTIAL_ROTATIONS`: none (used recovered durable config; no new credentials generated).
- Remaining limitation: commit/execution of this recovery changed image tags and container recreate timing only; no code/database migrations were altered.
### 7) Task status after recovery
- `DEV-000072_STATUS`: `PRODUCTION_ACCEPTANCE_BLOCKED_BY_DEPLOYMENT_INCIDENT` remains.
- Continue production readiness requires normal restart of production acceptance run from `docs/PROJECT_STATUS` after this incident checkpoint.
@@ -0,0 +1,61 @@
{
"artifact_id": "semantic-deferral-metamorphic-evidence-2026-08-17-v1",
"artifact_version": "semantic-deferral-metamorphic-evidence-v1",
"observed_at": "2026-08-17T21:10:00+03:00",
"task_id": "DEV-000072",
"focus": "semantic interpretation deferral boundary",
"metamorphic_claim": "Punctuation-free Russian interrogatives must not be finalized as human assertions solely because they lack '?'.",
"raw_model_evidence": [
{
"path": "RAW_CPU_QWEN_DIRECT",
"endpoint": "http://docker.cin.su:18082/",
"model_observed": "Qwen3 4B Instruct 2507",
"utterance": "ты кто",
"result": "understood as question"
},
{
"path": "RAW_CPU_QWEN_DIRECT",
"endpoint": "http://docker.cin.su:18082/",
"model_observed": "Qwen3 4B Instruct 2507",
"utterance": "можешь рассказать об утре",
"result": "semantic interpretation/clarification rather than factual assertion"
},
{
"path": "RAW_GPU_GEMMA4_DIRECT",
"endpoint": "http://192.168.220.91:11434",
"model_observed": "gemma4:26b",
"utterance": "ты кто",
"result": "understood as question"
},
{
"path": "RAW_GPU_GEMMA4_DIRECT",
"endpoint": "http://192.168.220.91:11434",
"model_observed": "gemma4:26b",
"utterance": "можешь расказать об утре",
"result": "coherent Russian response to punctuation-free natural request"
}
],
"domovoy_boundary_evidence": {
"before_fix": [
{
"utterance": "ты кто",
"semantic_path": "ASSERTION",
"cognition_requested": false
},
{
"utterance": "ты кто?",
"semantic_path": "QUESTION",
"cognition_requested": true
}
],
"interpretation": "The failure localizes to Domovoy's pre-cognition semantic path rather than to inability of either tested model to understand punctuation-free Russian."
},
"non_goal_evidence": {
"raw_model_identity_behavior": [
"Qwen identifies itself as Qwen/Alibaba and may invent a local name",
"Gemma identifies itself as a Google model"
],
"interpretation": "This is raw-model provenance behavior, not Domovoy identity semantics, and must not be patched inside Interaction with hardcoded response handlers."
},
"next_required_boundary": "After semantic deferral is corrected, a later milestone must inspect Domovoy's compiled context/system representation supplied to cognition without forcing the model to pretend it is Domovoy."
}
+525 -26
View File
@@ -58,7 +58,8 @@ func NewHumanUtterance(conversation interaction.Conversation, request interactio
return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err
}
millis := now.UnixMilli()
speechAct := ClassifySpeechAct(message, request.CorrectsUtteranceID)
candidate := BuildDeterministicCandidate(message, request.CorrectsUtteranceID, millis)
speechAct := candidate.PrimarySpeechAct
utterance := interaction.HumanUtterance{
ID: interaction.HumanUtteranceID(utteranceID),
ConversationID: conversation.ID,
@@ -97,31 +98,7 @@ func NewHumanUtterance(conversation interaction.Conversation, request interactio
}
func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct {
lower := strings.ToLower(strings.TrimSpace(message))
switch {
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
return interaction.SpeechActCorrection
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто ") || strings.Contains(lower, "объясни"):
return interaction.SpeechActQuestion
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
return interaction.SpeechActRequestToInvestigate
case strings.Contains(lower, "сделай") || strings.Contains(lower, "запусти") || strings.Contains(lower, "создай") || strings.Contains(lower, "настрой") || strings.Contains(lower, "добавь") || strings.Contains(lower, "измени") || strings.Contains(lower, "поменяй") || strings.Contains(lower, "используй") || strings.Contains(lower, "удали") || strings.Contains(lower, "удалить") || strings.Contains(lower, "напомни") || strings.Contains(lower, "подключи"):
return interaction.SpeechActDirective
case strings.HasPrefix(lower, "я решил") || strings.Contains(lower, "решение"):
return interaction.SpeechActDecision
case strings.Contains(lower, "предпочита") || strings.Contains(lower, "лучше"):
return interaction.SpeechActPreference
case strings.HasPrefix(lower, "думаю") || strings.Contains(lower, "гипотез"):
return interaction.SpeechActHypothesis
case lower == "да" || strings.HasPrefix(lower, "да,") || strings.Contains(lower, "подтверж"):
return interaction.SpeechActConfirmation
case lower == "нет" || strings.HasPrefix(lower, "нет,") || strings.Contains(lower, "отмен"):
return interaction.SpeechActRejection
case strings.Contains(lower, "привет") || strings.Contains(lower, "здравств"):
return interaction.SpeechActConversational
default:
return interaction.SpeechActAssertion
}
return BuildDeterministicCandidate(message, corrects, time.Now().UnixMilli()).PrimarySpeechAct
}
func NeedsCognition(act interaction.SpeechAct) bool {
@@ -142,3 +119,525 @@ func FallbackText(act interaction.SpeechAct) string {
return "Сообщение записано."
}
}
func BuildDeterministicCandidate(message string, corrects interaction.HumanUtteranceID, millis int64) interaction.SemanticCandidate {
id, _ := ids.New()
tokens := semanticTokens(message)
hasQuestionMark := strings.Contains(message, "?")
scores := semanticScores(tokens, hasQuestionMark, corrects != "")
if isIdentityNamingPreferenceDirective(tokens) {
scores[interaction.SpeechActDirective] += 5
scores[interaction.SpeechActPreference] += 4
}
primary, primaryScore := topAct(scores)
secondary, secondaryScore := secondAct(scores, primary)
if (startsWithModalRequest(tokens) || startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж")) &&
(primary == interaction.SpeechActAssertion || primary == interaction.SpeechActDirective) &&
(secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate) {
primary, secondary = secondary, primary
}
if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) &&
!startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") {
if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown
} else {
primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown
}
}
candidate := interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(id),
UtteranceID: "",
Source: interaction.CandidateSourceDeterministic,
SelectionState: interaction.CandidateObserved,
PrimarySpeechAct: primary,
SecondarySpeechAct: secondary,
ConfidenceMillis: boundedConfidence(primaryScore, secondaryScore, scores),
Applicability: applicabilityFor(primary, secondary, primaryScore, secondaryScore, tokens, corrects != ""),
RequestedOutcome: requestedOutcome(message, primary),
Clarification: interaction.ClarificationNotRequired,
NeedsCognition: needsCognitionForCandidate(primary, secondary, tokens),
SelectionReason: deterministicReason(primary, secondary, tokens),
CapabilityHints: capabilityHintsFor(primary, secondary, tokens),
RecordedAtUnixMillis: millis,
}
if candidate.PrimarySpeechAct == interaction.SpeechActUnknown {
candidate.PrimarySpeechAct = interaction.SpeechActAssertion
}
if startsWithDirectiveWhatNibud(tokens) {
candidate.NeedsCognition = false
candidate.CapabilityHints = removeCapabilityHint(candidate.CapabilityHints, "COGNITIVE_REASONING")
}
if candidate.Applicability == interaction.SemanticCandidateClarify {
candidate.Clarification = interaction.ClarificationRequired
candidate.NeedsCognition = false
}
return candidate
}
func isIdentityNamingPreferenceDirective(tokens []string) bool {
if !containsToken(tokens, "ты") && !containsToken(tokens, "тебя") && !containsToken(tokens, "тебе") && !containsToken(tokens, "вам") {
return false
}
if containsStemInTokens(tokens, "запомни", "назови", "назвать", "назвав", "зови", "название", "называ", "обраща", "обращай", "обращайся", "звать", "зов", "зовут") {
return true
}
if containsStemInTokens(tokens, "домов", "assist", "ассист", "assistant", "assistant") {
return true
}
if containsStemInTokens(tokens, "буду", "будешь") && containsStemInTokens(tokens, "звать", "обращ", "называть") {
return true
}
return false
}
func semanticTokens(message string) []string {
lower := strings.ToLower(strings.TrimSpace(message))
replacer := strings.NewReplacer(".", " ", ",", " ", "!", " ", "?", " ", ":", " ", ";", " ", "(", " ", ")", " ", "\"", " ", "'", " ", "«", " ", "»", " ", "+", " + ", "-", " - ", "/", " / ")
normalized := replacer.Replace(lower)
return strings.Fields(normalized)
}
func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bool) map[interaction.SpeechAct]int {
scores := map[interaction.SpeechAct]int{
interaction.SpeechActAssertion: 1,
interaction.SpeechActDirective: 0,
interaction.SpeechActQuestion: 0,
interaction.SpeechActRequestToInvestigate: 0,
interaction.SpeechActCorrection: 0,
interaction.SpeechActDecision: 0,
interaction.SpeechActPreference: 0,
interaction.SpeechActHypothesis: 0,
interaction.SpeechActConfirmation: 0,
interaction.SpeechActRejection: 0,
interaction.SpeechActConversational: 0,
}
if hasQuestionMark {
scores[interaction.SpeechActQuestion]++
}
if explicitCorrection {
scores[interaction.SpeechActCorrection] += 6
}
first := ""
if len(tokens) > 0 {
first = tokens[0]
}
if first == "нет" || first == "не" {
scores[interaction.SpeechActCorrection] += 2
scores[interaction.SpeechActRejection]++
}
if first == "да" {
scores[interaction.SpeechActConfirmation] += 2
}
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "подум", "понят", "посмотри"}
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж"}
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"}
decisionStems := []string{"решил", "решение", "выбираю"}
arithmeticMarker := false
for index, token := range tokens {
if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") {
scores[interaction.SpeechActQuestion] += 3
}
if containsStem(token, conversationWords...) {
scores[interaction.SpeechActConversational] += 3
}
if containsStem(token, investigateStems...) {
scores[interaction.SpeechActRequestToInvestigate] += 3
scores[interaction.SpeechActQuestion]++
}
if containsStem(token, directiveStems...) {
scores[interaction.SpeechActDirective] += 3
}
if containsStem(token, preferenceStems...) {
scores[interaction.SpeechActPreference] += 3
}
if containsStem(token, hypothesisStems...) {
scores[interaction.SpeechActHypothesis] += 3
}
if containsStem(token, decisionStems...) {
scores[interaction.SpeechActDecision] += 3
}
if token == "+" || token == "-" || token == "/" || token == "*" || token == "плюс" || token == "минус" {
arithmeticMarker = true
}
}
if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") {
scores[interaction.SpeechActAssertion] += 5
}
if arithmeticMarker {
scores[interaction.SpeechActQuestion] += 2
scores[interaction.SpeechActDirective]++
}
if len(tokens) <= 4 {
for index, token := range tokens {
if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") {
scores[interaction.SpeechActQuestion]++
}
}
}
if len(tokens) == 2 && tokens[0] == "ты" {
scores[interaction.SpeechActQuestion] += 2
}
if len(tokens) >= 2 && tokens[0] == "как" && containsToken(tokens, "зовут") {
scores[interaction.SpeechActQuestion] += 2
}
if startsWithDirectiveWhatNibud(tokens) {
scores[interaction.SpeechActDirective] += 4
}
if len(tokens) > 0 && containsStem(tokens[0], investigateStems...) {
scores[interaction.SpeechActRequestToInvestigate] += 2
}
if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") && containsMeaningfulInterrogative(tokens) {
scores[interaction.SpeechActRequestToInvestigate] += 2
}
if startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) {
scores[interaction.SpeechActQuestion] += 2
scores[interaction.SpeechActRequestToInvestigate] += 3
}
if startsWithModalRequest(tokens) && containsStemInTokens(tokens, explanatoryRequestStems...) {
scores[interaction.SpeechActQuestion] += 3
scores[interaction.SpeechActRequestToInvestigate] += 3
}
if containsModelReference(tokens) {
if containsStemInTokens(tokens, "обрати", "спрос", "уточни") {
scores[interaction.SpeechActDirective] += 3
scores[interaction.SpeechActQuestion] += 2
}
}
if containsStemInTokens(tokens, "замени", "смени") {
scores[interaction.SpeechActDirective] += 3
}
if !hasQuestionMark && !containsInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) &&
scores[interaction.SpeechActDirective] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 && scores[interaction.SpeechActCorrection] == 0 {
scores[interaction.SpeechActAssertion] += 2
}
return scores
}
func topAct(scores map[interaction.SpeechAct]int) (interaction.SpeechAct, int) {
bestAct, bestScore := interaction.SpeechActUnknown, -1
for _, act := range []interaction.SpeechAct{
interaction.SpeechActQuestion,
interaction.SpeechActRequestToInvestigate,
interaction.SpeechActDirective,
interaction.SpeechActCorrection,
interaction.SpeechActConversational,
interaction.SpeechActPreference,
interaction.SpeechActHypothesis,
interaction.SpeechActDecision,
interaction.SpeechActConfirmation,
interaction.SpeechActRejection,
interaction.SpeechActAssertion,
} {
if score := scores[act]; score > bestScore {
bestAct, bestScore = act, score
}
}
return bestAct, bestScore
}
func secondAct(scores map[interaction.SpeechAct]int, primary interaction.SpeechAct) (interaction.SpeechAct, int) {
second, score := interaction.SpeechActUnknown, -1
for act, value := range scores {
if act == primary {
continue
}
if value > score {
second, score = act, value
}
}
if score <= 0 {
return interaction.SpeechActUnknown, 0
}
return second, score
}
func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, secondaryScore int, tokens []string, explicitCorrection bool) interaction.SemanticApplicability {
if primary == interaction.SpeechActAssertion && !containsInterrogative(tokens) && secondaryScore <= 1 {
return interaction.SemanticApplicable
}
if primaryScore < 2 {
return interaction.SemanticNeedsBroaderInterpret
}
if explicitCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) {
return interaction.SemanticMixedIntent
}
if primary == interaction.SpeechActCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) {
return interaction.SemanticMixedIntent
}
if primary == interaction.SpeechActAssertion && containsMeaningfulInterrogative(tokens) {
return interaction.SemanticNeedsBroaderInterpret
}
if primary == interaction.SpeechActDirective && likelyConsequential(tokens) {
return interaction.SemanticCandidateClarify
}
if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "обрати", "спрос") && containsModelReference(tokens) {
return interaction.SemanticLowConfidence
}
if primaryScore-secondaryScore <= 1 && secondaryScore >= 2 {
return interaction.SemanticLowConfidence
}
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate {
if primaryScore < 4 {
return interaction.SemanticNeedsBroaderInterpret
}
}
return interaction.SemanticApplicable
}
func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens []string) bool {
if primary == interaction.SpeechActDirective && secondary == interaction.SpeechActPreference && isIdentityNamingPreferenceDirective(tokens) {
return false
}
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate {
return true
}
if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate {
return true
}
if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) {
return true
}
return false
}
func deterministicReason(primary, secondary interaction.SpeechAct, tokens []string) string {
reasons := []string{"deterministic candidate from token-shape, interrogative, imperative and context cues"}
if primary == interaction.SpeechActQuestion && containsInterrogative(tokens) {
reasons = append(reasons, "interrogative structure observed")
}
if secondary != interaction.SpeechActUnknown {
reasons = append(reasons, "secondary competing semantic signal observed")
}
return strings.Join(reasons, "; ")
}
func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []string) []string {
seen := map[string]struct{}{}
add := func(kind string) {
if kind != "" {
seen[kind] = struct{}{}
}
}
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate {
add("COGNITIVE_REASONING")
}
if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") {
add("SCHEDULE_REMINDER")
}
if isExplicitEmailAction(tokens) {
add("SEND_EMAIL")
}
if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") {
if primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActDirective {
add("MODIFY_CODE")
add("RUN_TESTS")
}
}
if containsToken(tokens, "assistant") && containsStemInTokens(tokens, "исправ", "добав", "измени", "помен", "патч") {
add("MODIFY_CODE")
add("RUN_TESTS")
}
if containsStemInTokens(tokens, "исправ", "поправ") && (containsToken(tokens, "чате") || containsToken(tokens, "текст") || containsToken(tokens, "assistant")) {
add("MODIFY_CODE")
add("RUN_TESTS")
}
if containsStemInTokens(tokens, "патч", "исправ", "поправ") && containsStemInTokens(tokens, "разбер", "проверь", "почему", "тормозит", "подум") {
add("MODIFY_CODE")
add("RUN_TESTS")
}
if containsInterrogative(tokens) && containsArithmetic(tokens) {
add("COGNITIVE_REASONING")
}
if containsToken(tokens, "свободно") || containsToken(tokens, "гб") || containsToken(tokens, "модель") {
add("VERIFY_FACT")
}
values := make([]string, 0, len(seen))
for kind := range seen {
values = append(values, kind)
}
return values
}
func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 {
total := 0
for _, score := range scores {
if score > 0 {
total += score
}
}
if total == 0 {
return 200
}
confidence := int64(400 + (primaryScore * 100))
if secondaryScore > 0 {
confidence -= int64(secondaryScore * 40)
}
if confidence < 200 {
confidence = 200
}
if confidence > 980 {
confidence = 980
}
return int64(confidence)
}
func containsStem(token string, stems ...string) bool {
for _, stem := range stems {
if strings.Contains(token, stem) {
return true
}
}
return false
}
func containsStemInTokens(tokens []string, stems ...string) bool {
for _, token := range tokens {
if containsStem(token, stems...) {
return true
}
}
return false
}
func requestedOutcome(message string, act interaction.SpeechAct) string {
if act == interaction.SpeechActDirective || act == interaction.SpeechActDecision || act == interaction.SpeechActRequestToInvestigate {
return strings.TrimSpace(message)
}
return ""
}
func containsToken(tokens []string, expected string) bool {
for _, token := range tokens {
if token == expected {
return true
}
}
return false
}
func startsWithModalRequest(tokens []string) bool {
if len(tokens) == 0 {
return false
}
switch tokens[0] {
case "можешь", "можете", "сможешь", "сможете":
return true
}
if len(tokens) >= 3 && tokens[0] == "не" && strings.HasPrefix(tokens[1], "мог") && tokens[2] == "бы" {
return true
}
return false
}
func startsWithExplanatoryRequest(tokens []string, stems ...string) bool {
if len(tokens) == 0 {
return false
}
if containsStem(tokens[0], stems...) {
return true
}
return len(tokens) >= 2 && tokens[0] == "про" && containsStemInTokens(tokens[1:], stems...)
}
func containsInterrogative(tokens []string) bool {
for _, token := range tokens {
if token == "-" {
continue
}
switch token {
case "кто", "что", "как", "почему", "зачем", "где", "когда", "сколько", "какой", "какая", "какие", "чей":
return true
}
}
return false
}
func containsMeaningfulInterrogative(tokens []string) bool {
for index, token := range tokens {
if token == "что" && followsNibud(tokens[index+1:]) {
continue
}
switch token {
case "кто", "что", "как", "почему", "зачем", "где", "когда", "сколько", "какой", "какая", "какие", "чей":
return true
}
}
return false
}
func containsArithmetic(tokens []string) bool {
digits := 0
for _, token := range tokens {
if token == "+" || token == "-" || token == "*" || token == "/" || token == "плюс" || token == "минус" {
return true
}
for _, r := range token {
if r >= '0' && r <= '9' {
digits++
break
}
}
}
return digits >= 2
}
func likelyConsequential(tokens []string) bool {
for _, token := range tokens {
if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи") {
return true
}
}
return false
}
func followsNibud(tokens []string) bool {
for _, token := range tokens {
if token == "-" || token == "ни" {
continue
}
return token == "нибудь"
}
return false
}
func startsWithDirectiveWhatNibud(tokens []string) bool {
if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") {
return false
}
for index, token := range tokens {
if token == "что" && followsNibud(tokens[index+1:]) {
return true
}
}
return false
}
func containsModelReference(tokens []string) bool {
for _, token := range tokens {
if token == "модель" || token == "модели" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") {
return true
}
}
return false
}
func isExplicitEmailAction(tokens []string) bool {
hasMailObject := containsToken(tokens, "письмо") || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта")
hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш")
return hasMailObject && hasMailVerb
}
func removeCapabilityHint(values []string, denied string) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value == denied {
continue
}
result = append(result, value)
}
return result
}
+80
View File
@@ -49,6 +49,86 @@ func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t
}
}
func TestDeterministicCandidateIsInvariantToMissingQuestionMarkForIdentityAndArithmetic(t *testing.T) {
cases := []struct {
message string
act interaction.SpeechAct
}{
{"ты кто", interaction.SpeechActQuestion},
{"Ты кто?", interaction.SpeechActQuestion},
{"а ты кто", interaction.SpeechActQuestion},
{"сколько будет 2 + 2", interaction.SpeechActQuestion},
{"сколько будет 2 + 2?", interaction.SpeechActQuestion},
{"2 плюс 2 сколько", interaction.SpeechActQuestion},
}
for _, tc := range cases {
candidate := BuildDeterministicCandidate(tc.message, "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != tc.act {
t.Fatalf("expected %s for %q, got %s (%+v)", tc.act, tc.message, candidate.PrimarySpeechAct, candidate)
}
if candidate.Applicability == interaction.SemanticApplicable && candidate.NeedsCognition {
continue
}
if !candidate.NeedsCognition {
t.Fatalf("expected cognition-worthy candidate for %q, got %+v", tc.message, candidate)
}
}
}
func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWithoutQuestionMark(t *testing.T) {
for _, message := range []string{
"можешь рассказать об утре",
"можешь расказать об утре",
"можете объяснить это проще",
"расскажи об утре",
"про утро расскажешь",
} {
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActQuestion && candidate.PrimarySpeechAct != interaction.SpeechActRequestToInvestigate {
t.Fatalf("expected broader cognitive speech act for %q, got %+v", message, candidate)
}
if !candidate.NeedsCognition {
t.Fatalf("expected cognition for %q, got %+v", message, candidate)
}
}
}
func TestDeterministicCandidateClassifiesIdentityNamingPreferenceAsDirective(t *testing.T) {
for _, message := range []string{
"Запомни тебя зовут Домовой, я тебя буду звать Домовым",
"я буду звать тебя Домовым",
"давай я буду звать тебя Домовой",
"теперь буду обращаться к тебе как к Домовой",
"запомни, я называю тебя Домовым",
"для меня ты Домовой",
} {
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive for %q, got %+v", message, candidate)
}
if candidate.NeedsCognition {
t.Fatalf("expected no cognition for naming preference by default for %q, got %+v", message, candidate)
}
}
}
func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *testing.T) {
candidate := BuildDeterministicCandidate("обратись к модели gemma4 и спроси сколько звезд на небе", "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %+v", candidate)
}
if !candidate.NeedsCognition {
t.Fatalf("expected cognition-worthy model request, got %+v", candidate)
}
}
func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) {
candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActAssertion {
t.Fatalf("expected assertion, got %+v", candidate)
}
}
func TestNewConversationRequiresActorAndPrincipal(t *testing.T) {
_, err := NewConversation(identity.ActorID(""), identity.PrincipalID("principal-1"), time.Unix(10, 0))
if err == nil {
+103 -33
View File
@@ -11,34 +11,48 @@ import (
)
type SemanticResult struct {
Interpretation interaction.UtteranceInterpretation
Projects []interaction.ProjectReference
Relations []interaction.InteractionRelation
ActiveFocus *situation.ActiveProjectFocus
Reply string
NeedsCognition bool
Interpretation interaction.UtteranceInterpretation
Candidates []interaction.SemanticCandidate
Projects []interaction.ProjectReference
Relations []interaction.InteractionRelation
ActiveFocus *situation.ActiveProjectFocus
Reply string
NeedsCognition bool
CapabilityHints []string
}
func InterpretUtterance(utterance interaction.HumanUtterance, projects []situation.ProjectDescriptor, previousFocus *situation.ActiveProjectFocus, now time.Time) (SemanticResult, error) {
act := ClassifySpeechAct(utterance.Content, utterance.CorrectsUtteranceID)
interpretationID, err := ids.New()
if err != nil {
return SemanticResult{}, err
}
millis := now.UnixMilli()
deterministicCandidate := BuildDeterministicCandidate(utterance.Content, utterance.CorrectsUtteranceID, millis)
deterministicCandidate.UtteranceID = utterance.ID
selectedCandidate := deterministicCandidate
assertionVerification := string(epistemics.VerificationNotApplicable)
if selectedCandidate.PrimarySpeechAct == interaction.SpeechActAssertion {
assertionVerification = string(epistemics.VerificationUnverified)
}
result := SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
ID: interaction.InterpretationID(interpretationID),
UtteranceID: utterance.ID,
SpeechAct: act,
ConfidenceMillis: confidenceFor(act),
Source: "deterministic-v1",
AssertionVerification: string(epistemics.VerificationUnverified),
RequestedOutcome: requestedOutcome(utterance.Content, act),
Clarification: interaction.ClarificationNotRequired,
SpeechAct: selectedCandidate.PrimarySpeechAct,
SecondarySpeechAct: selectedCandidate.SecondarySpeechAct,
ConfidenceMillis: selectedCandidate.ConfidenceMillis,
Source: string(selectedCandidate.Source),
AssertionVerification: assertionVerification,
RequestedOutcome: selectedCandidate.RequestedOutcome,
Clarification: selectedCandidate.Clarification,
NeedsCognition: selectedCandidate.NeedsCognition,
Applicability: selectedCandidate.Applicability,
SelectionReason: selectedCandidate.SelectionReason,
RecordedAtUnixMillis: millis,
},
NeedsCognition: NeedsCognition(act),
Candidates: []interaction.SemanticCandidate{deterministicCandidate},
NeedsCognition: selectedCandidate.NeedsCognition,
CapabilityHints: append([]string(nil), selectedCandidate.CapabilityHints...),
}
explicitFocus := explicitProjectFocus(utterance.Content, projects)
projectRefs, err := projectReferences(utterance, projects, explicitFocus, previousFocus, millis)
@@ -58,12 +72,15 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
UpdatedAtUnixMillis: millis,
}
}
if isAmbiguousDestructive(act, utterance.Content, result.Projects) {
if selectedCandidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(selectedCandidate, utterance.Content, result.Projects) {
result.Interpretation.Clarification = interaction.ClarificationRequired
result.Interpretation.Applicability = interaction.SemanticCandidateClarify
result.Interpretation.SelectionReason = "consequential ambiguity requires clarification before relying on cheap deterministic interpretation"
result.NeedsCognition = false
result.Reply = "Уточните проект и цель: это потенциально разрушительное изменение, а текущая ссылка недостаточно надёжна."
return result, nil
}
if act == interaction.SpeechActCorrection {
if hasCorrectionMeaning(selectedCandidate, utterance) {
relations, err := correctionRelations(utterance, millis)
if err != nil {
return SemanticResult{}, err
@@ -71,12 +88,56 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
result.Relations = append(result.Relations, relations...)
}
result.Reply = semanticReply(utterance.Content, result, previousFocus)
if isCurrentFocusQuestion(utterance.Content, act) && previousFocus != nil {
if isCurrentFocusQuestion(utterance.Content, selectedCandidate.PrimarySpeechAct) && previousFocus != nil {
result.NeedsCognition = false
result.Interpretation.NeedsCognition = false
}
return result, nil
}
func ApplyCognitiveCandidate(result SemanticResult, message string, candidate interaction.SemanticCandidate, previousFocus *situation.ActiveProjectFocus) SemanticResult {
candidate.SelectionState = interaction.CandidateSelected
if candidate.UtteranceID == "" {
candidate.UtteranceID = result.Interpretation.UtteranceID
}
selected := false
for index := range result.Candidates {
if result.Candidates[index].SelectionState == interaction.CandidateSelected {
result.Candidates[index].SelectionState = interaction.CandidateObserved
}
if result.Candidates[index].ID == candidate.ID {
result.Candidates[index] = candidate
selected = true
}
}
if !selected {
result.Candidates = append(result.Candidates, candidate)
}
result.Interpretation.SpeechAct = candidate.PrimarySpeechAct
result.Interpretation.SecondarySpeechAct = candidate.SecondarySpeechAct
result.Interpretation.ConfidenceMillis = candidate.ConfidenceMillis
result.Interpretation.Source = string(candidate.Source)
result.Interpretation.RequestedOutcome = candidate.RequestedOutcome
result.Interpretation.Clarification = candidate.Clarification
result.Interpretation.NeedsCognition = candidate.NeedsCognition
result.Interpretation.Applicability = candidate.Applicability
result.Interpretation.SelectionReason = candidate.SelectionReason
result.NeedsCognition = candidate.NeedsCognition
result.CapabilityHints = append([]string(nil), candidate.CapabilityHints...)
if result.Interpretation.SpeechAct == interaction.SpeechActAssertion {
result.Interpretation.AssertionVerification = string(epistemics.VerificationUnverified)
} else {
result.Interpretation.AssertionVerification = string(epistemics.VerificationNotApplicable)
}
if candidate.Applicability == interaction.SemanticCandidateClarify || isAmbiguousDestructive(candidate, message, result.Projects) {
result.Interpretation.Clarification = interaction.ClarificationRequired
result.NeedsCognition = false
result.Interpretation.NeedsCognition = false
}
result.Reply = semanticReply(message, result, previousFocus)
return result
}
func explicitProjectFocus(message string, projects []situation.ProjectDescriptor) *interaction.ProjectReference {
lower := strings.ToLower(message)
if !(strings.Contains(lower, "работаем с") || strings.Contains(lower, "вернемся к") || strings.Contains(lower, "вернёмся к") || strings.HasPrefix(strings.TrimSpace(lower), "теперь ")) {
@@ -181,20 +242,6 @@ func correctionRelations(utterance interaction.HumanUtterance, millis int64) ([]
return []interaction.InteractionRelation{{ID: interaction.InteractionRelationID(id), FromUtteranceID: utterance.ID, ToUtteranceID: utterance.CorrectsUtteranceID, Kind: interaction.RelationSupersedes, RecordedAtUnixMillis: millis}}, nil
}
func confidenceFor(act interaction.SpeechAct) int64 {
if act == interaction.SpeechActUnknown {
return 300
}
return 800
}
func requestedOutcome(message string, act interaction.SpeechAct) string {
if act == interaction.SpeechActDirective || act == interaction.SpeechActDecision || act == interaction.SpeechActRequestToInvestigate {
return strings.TrimSpace(message)
}
return ""
}
func inheritedFocusAllowed(act interaction.SpeechAct, message string) bool {
lower := strings.ToLower(message)
if act == interaction.SpeechActUnknown {
@@ -209,10 +256,10 @@ func inheritedFocusAllowed(act interaction.SpeechAct, message string) bool {
return false
}
func isAmbiguousDestructive(act interaction.SpeechAct, message string, projects []interaction.ProjectReference) bool {
func isAmbiguousDestructive(candidate interaction.SemanticCandidate, message string, projects []interaction.ProjectReference) bool {
lower := strings.ToLower(message)
destructive := strings.Contains(lower, "удали") || strings.Contains(lower, "снеси") || strings.Contains(lower, "удалить")
if !destructive || act != interaction.SpeechActDirective {
if !destructive || (candidate.PrimarySpeechAct != interaction.SpeechActDirective && candidate.SecondarySpeechAct != interaction.SpeechActDirective) {
return false
}
if len(projects) == 0 {
@@ -227,18 +274,34 @@ func semanticReply(message string, result SemanticResult, previousFocus *situati
}
switch result.Interpretation.SpeechAct {
case interaction.SpeechActAssertion:
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence {
return "Смысл сообщения пока понят недостаточно надёжно. Я сохраню его и при доступности более широкого когнитивного разбора не буду притворяться, что это уже проверенный факт."
}
return "Записал как утверждение человека. Это пока не проверенный факт."
case interaction.SpeechActDirective:
if result.Interpretation.SecondarySpeechAct == interaction.SpeechActCorrection {
return "Понял это как исправление предыдущего понимания и одновременно как новую просьбу. Ничего не выполнено без отдельного подтверждённого действия."
}
return "Записал просьбу как намерение пользователя. Ничего не выполнено без отдельного подтверждённого действия."
case interaction.SpeechActDecision:
return "Записал это как явное решение пользователя для дальнейшей обработки."
case interaction.SpeechActCorrection:
if result.Interpretation.SecondarySpeechAct == interaction.SpeechActDirective || result.Interpretation.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate {
return "Исправление записано отдельно, и я вижу здесь ещё новую рабочую просьбу; история сохранена без переписывания."
}
return "Исправление записано новым сообщением; прежняя история сохранена."
case interaction.SpeechActQuestion:
lower := strings.ToLower(message)
if previousFocus != nil && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")) {
return "Сейчас в фокусе: " + previousFocus.ProjectDisplayName + "."
}
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence {
return "Похоже, здесь нужен более широкий разбор смысла, а не дешёвый поверхностный рефлекс. Я не буду делать вид, что уже понял всё достаточно точно."
}
case interaction.SpeechActRequestToInvestigate:
if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence {
return "Похоже, здесь запрошено рассуждение или исследование. Я не буду финализировать смысл только по поверхностным признакам."
}
}
return FallbackText(result.Interpretation.SpeechAct)
}
@@ -250,3 +313,10 @@ func isCurrentFocusQuestion(message string, act interaction.SpeechAct) bool {
lower := strings.ToLower(message)
return strings.Contains(lower, "сейчас") || strings.Contains(lower, "что мы") || strings.Contains(lower, "делаем")
}
func hasCorrectionMeaning(candidate interaction.SemanticCandidate, utterance interaction.HumanUtterance) bool {
if utterance.CorrectsUtteranceID != "" {
return true
}
return candidate.PrimarySpeechAct == interaction.SpeechActCorrection || candidate.SecondarySpeechAct == interaction.SpeechActCorrection
}
@@ -66,6 +66,16 @@ func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) {
}
}
func TestInterpretUtteranceIdentityNamingPreferenceNotAssertion(t *testing.T) {
result := interpretForTest(t, "Запомни тебя зовут Домовой, я тебя буду звать Домовым", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive for naming preference, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.AssertionVerification == "UNVERIFIED" {
t.Fatalf("expected no assertion semantics path for naming preference, got %#v", result.Interpretation)
}
}
func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "")
if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {
@@ -116,6 +126,48 @@ func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.
}
}
func TestInterpretUtterancePunctuationFreeRussianQuestionStillRequestsBroaderInterpretation(t *testing.T) {
result := interpretForTest(t, "ты кто", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for punctuation-free identity question, got %#v", result)
}
}
func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *testing.T) {
result := interpretForTest(t, "сколько будет 2 + 2", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for punctuation-free arithmetic, got %#v", result)
}
}
func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) {
for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} {
result := interpretForTest(t, message, nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
t.Fatalf("expected cognition-worthy interpretation for %q, got %#v", message, result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for %q, got %#v", message, result)
}
}
}
func TestInterpretUtteranceNamedModelRequestIsDirectiveNeedingCognition(t *testing.T) {
result := interpretForTest(t, "обратись к модели gemma4 и спроси сколько звезд на небе", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for named-model request, got %#v", result)
}
}
func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult {
t.Helper()
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}
+84 -26
View File
@@ -74,21 +74,51 @@ func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.Planning
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "объясни", "расскажи", "что делает"):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case containsAny(lower, "напомни", "напомин"):
case input.RequiresClarification:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "clarification is required before capability selection or durable work", Reply: "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."}
case isReminderRequest(lower):
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
case containsAny(lower, "отправь", "письм", "почт"):
case isEmailReviewRequest(lower):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "mail triage/review needs structured reading and reasoning without claiming outbound send"}
case isExplicitEmailGapRequest(lower):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."}
case containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
case isUnsupportedCapabilityRequest(lower):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
case needsDurableWorkFromHints(input):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"}
case requiresDurableWork(lower):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
case containsAny(lower, "объясни", "расскажи", "что делает"):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case input.NeedsCognition && needsCognitiveAnswer(input):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant and no durable work path outranks it"}
case input.NeedsCognition && (input.SpeechAct == work.SpeechActRef("QUESTION") || input.SpeechAct == work.SpeechActRef("REQUEST_TO_INVESTIGATE")):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "semantic layer marked this utterance as cognition-relevant"}
default:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
}
}
func needsDurableWorkFromHints(input work.PlanningInput) bool {
for _, hint := range input.CapabilityHints {
switch hint {
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
return true
}
}
return false
}
func needsCognitiveAnswer(input work.PlanningInput) bool {
for _, hint := range input.CapabilityHints {
if hint == capability.CapabilityKindCognitiveReasoning {
return true
}
}
lower := strings.ToLower(input.Message)
return containsAny(lower, "модель", "gemma4", "qwen", "спроси", "обратись", "посчитай")
}
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
return ErrInvalidPlanCandidate
@@ -122,42 +152,52 @@ func ValidatePlanCandidate(candidate work.PlanCandidate) error {
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
lower := strings.ToLower(input.Message)
if input.SecondarySpeechAct == work.SpeechActRef("CORRECTION") {
addStep(candidate, work.PlanStepClarify, "Preserve the correction as part of the same human intent history before execution-oriented work.", len(candidate.Steps)+1, millis)
}
switch {
case isReminderRequest(lower):
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
case isEmailReviewRequest(lower):
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
case isExplicitEmailGapRequest(lower):
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
if containsAny(lower, "патч", "исправ") {
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software after investigation confirms the change", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindRunTests, "validate the patch after code modification", epistemics.VerificationNotApplicable, millis)
}
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
if containsAny(lower, "патч", "исправ") {
addStep(candidate, work.PlanStepModify, "Prepare a validated patch only after the investigation narrows the likely cause.", 4, millis)
addStep(candidate, work.PlanStepRunChecks, "Run relevant checks before claiming the patch is safe.", 5, millis)
} else {
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
}
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
addStep(candidate, work.PlanStepModify, "Prepare a code-change work item for a capable coding executor.", 2, millis)
addStep(candidate, work.PlanStepRunChecks, "Run relevant tests and architecture checks before outcome claim.", 3, millis)
case containsAny(lower, "письм", "почт"):
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
if containsAny(lower, "ответ", "ответить", "скажи") {
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
}
if containsAny(lower, "отправь", "напиши") {
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
}
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
case containsAny(lower, "напомни", "завтра", "напомин"):
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
case containsAny(lower, "отправь", "ивану", "сообщи"):
addRequirement(candidate, capability.CapabilityKindSendEmail, "communicate with the named recipient after channel/authority resolution", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
default:
for _, hint := range input.CapabilityHints {
addRequirement(candidate, hint, "semantic candidate hinted that this capability may be needed", epistemics.VerificationUnknown, millis)
}
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "clarify/decompose ambiguous human intent", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepClarify, "Clarify desired outcome and required capabilities.", 1, millis)
candidate.RequiresClarification = true
@@ -168,7 +208,7 @@ func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.Pla
}
func requiresDurableWork(lower string) bool {
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "письм", "почт", "отправь", "ивану", "сообщи", "установи", "большую модель")
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "патч", "исправь", "установи", "большую модель")
}
func addRequirement(candidate *work.PlanCandidate, kind capability.CapabilityKind, purpose string, precondition epistemics.VerificationState, millis int64) {
@@ -257,3 +297,21 @@ func containsAny(value string, needles ...string) bool {
}
return false
}
func isReminderRequest(lower string) bool {
return containsAny(lower, "напомни", "напомин")
}
func isEmailReviewRequest(lower string) bool {
return containsAny(lower, "посмотри новые письма", "какие нужно ответить", "на какие нужно ответить", "разбери почту")
}
func isExplicitEmailGapRequest(lower string) bool {
hasMailObject := containsAny(lower, "письмо", "письма", "почту", "почта")
hasMailAction := containsAny(lower, "отправь", "ответить", "ответь", "напиши")
return hasMailObject && hasMailAction
}
func isUnsupportedCapabilityRequest(lower string) bool {
return containsAny(lower, "не умеешь", "подключи интеграцию", "подключи сервис", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер", "сервис, которого сейчас нет")
}
@@ -92,3 +92,37 @@ func TestProductionAuditArtifactExists(t *testing.T) {
t.Fatalf("unexpected audit facts: %+v", artifact)
}
}
func TestSemanticDeferralMetamorphicEvidenceExists(t *testing.T) {
payload, err := os.ReadFile("../../docs/evidence/semantic-deferral-metamorphic-evidence-2026-08-17-v1.json")
if err != nil {
t.Fatal(err)
}
var artifact struct {
ArtifactID string `json:"artifact_id"`
TaskID string `json:"task_id"`
RawModelEvidence []struct {
Utterance string `json:"utterance"`
Result string `json:"result"`
} `json:"raw_model_evidence"`
DomovoyBoundaryEvidence struct {
BeforeFix []struct {
Utterance string `json:"utterance"`
SemanticPath string `json:"semantic_path"`
CognitionRequested bool `json:"cognition_requested"`
} `json:"before_fix"`
} `json:"domovoy_boundary_evidence"`
}
if err := json.Unmarshal(payload, &artifact); err != nil {
t.Fatal(err)
}
if artifact.ArtifactID != "semantic-deferral-metamorphic-evidence-2026-08-17-v1" || artifact.TaskID != "DEV-000072" {
t.Fatalf("unexpected semantic deferral evidence identity: %+v", artifact)
}
if len(artifact.RawModelEvidence) < 4 {
t.Fatalf("expected CPU and GPU raw evidence, got %+v", artifact.RawModelEvidence)
}
if len(artifact.DomovoyBoundaryEvidence.BeforeFix) < 2 || artifact.DomovoyBoundaryEvidence.BeforeFix[0].SemanticPath != "ASSERTION" || artifact.DomovoyBoundaryEvidence.BeforeFix[0].CognitionRequested {
t.Fatalf("expected pre-fix Domovoy boundary evidence, got %+v", artifact.DomovoyBoundaryEvidence)
}
}
+16
View File
@@ -382,7 +382,11 @@ func runCase(item Case, projects []ProjectDescriptor) (CaseResult, error) {
PrincipalID: utterance.PrincipalID,
Message: utterance.Content,
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
SecondarySpeechAct: work.SpeechActRef(semantic.Interpretation.SecondarySpeechAct),
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
CapabilityHints: semanticCapabilityKinds(semantic.CapabilityHints),
NeedsCognition: semantic.NeedsCognition,
RequiresClarification: semantic.Interpretation.Clarification == interaction.ClarificationRequired,
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
if len(semantic.Projects) > 0 {
@@ -821,3 +825,15 @@ func replyMentionsAny(reply string, markers []string) bool {
}
return false
}
func semanticCapabilityKinds(values []string) []capability.CapabilityKind {
result := make([]capability.CapabilityKind, 0, len(values))
for _, value := range values {
kind := capability.CapabilityKind(strings.TrimSpace(value))
if kind == "" {
continue
}
result = append(result, kind)
}
return result
}
+6 -4
View File
@@ -43,11 +43,13 @@ func TestRealityEvaluationBaselineProducesInspectableSignals(t *testing.T) {
if artifact.SolvedAutonomously == 0 || artifact.GenuineCapabilityGaps == 0 {
t.Fatalf("expected multiple baseline buckets, got %+v", artifact)
}
if artifact.ConsequenceReasoningFailures == 0 {
t.Fatalf("expected at least one consequence reasoning failure in current baseline: %+v", artifact)
if artifact.FailedWithAvailableCapability == 0 && artifact.ConsequenceReasoningFailures == 0 && artifact.IntentFailures == 0 {
if artifact.CorrectHumanIntervention == 0 || artifact.GenuineCapabilityGaps == 0 {
t.Fatalf("expected inspectable routing signals even with zero current failures: %+v", artifact)
}
}
if len(artifact.WorstFailures) == 0 {
t.Fatalf("expected worst failures to be captured")
if artifact.FailedWithAvailableCapability > 0 && len(artifact.WorstFailures) == 0 {
t.Fatalf("expected worst failures to be captured when failures remain")
}
}