Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754cbe43d2 | ||
|
|
36fec6c05c | ||
|
|
11a6a23f6e | ||
|
|
ce666bbc43 | ||
|
|
64870798b9 |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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] + "..."
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user