Compare commits
378
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8037e66998 | ||
|
|
29815c49b8 | ||
|
|
c14ad7fd0b | ||
|
|
939a5152e1 | ||
|
|
4e54ace76b | ||
|
|
f329224d12 | ||
|
|
bb76171af4 | ||
|
|
213f06661d | ||
|
|
d350b9a565 | ||
|
|
f862c3ac13 | ||
|
|
5e18eb6082 | ||
|
|
ec86c983e3 | ||
|
|
ea854d5278 | ||
|
|
bbecadcec2 | ||
|
|
1c89b30512 | ||
|
|
75a8533939 | ||
|
|
515a98c470 | ||
|
|
d3ad1fd5bb | ||
|
|
e1802aef12 | ||
|
|
35fc2438dc | ||
|
|
70335a78e2 | ||
|
|
41067a0b16 | ||
|
|
fba05d5ed4 | ||
|
|
6e14ba1727 | ||
|
|
9c6e2097b1 | ||
|
|
96559f6ab8 | ||
|
|
46b89577eb | ||
|
|
a6ddbfaaf2 | ||
|
|
362a1bd993 | ||
|
|
735222de0d | ||
|
|
4e3d153557 | ||
|
|
f316f7ec3f | ||
|
|
da5d06895b | ||
|
|
4d1a579132 | ||
|
|
43836af511 | ||
|
|
32457f7142 | ||
|
|
204eee489c | ||
|
|
5cd8665aba | ||
|
|
04018cbd07 | ||
|
|
23a1eeae56 | ||
|
|
369588ada7 | ||
|
|
e323689fd4 | ||
|
|
af042dc69f | ||
|
|
60c1c31bd9 | ||
|
|
ff3f14fc77 | ||
|
|
36f991d8d0 | ||
|
|
54a3996c17 | ||
|
|
a769f33d16 | ||
|
|
056ff1b795 | ||
|
|
7b2debb60e | ||
|
|
6dfb3fc26c | ||
|
|
e0a093eaf3 | ||
|
|
24916aa8f7 | ||
|
|
d7afd5be34 | ||
|
|
e104ce790d | ||
|
|
d7484cf757 | ||
|
|
a57e8ea71b | ||
|
|
9dffde63b8 | ||
|
|
4e5bb9abc9 | ||
|
|
3435de8013 | ||
|
|
76c2824887 | ||
|
|
fc00a57942 | ||
|
|
1b21888e8b | ||
|
|
012f5efafe | ||
|
|
eb842e24ff | ||
|
|
13bbcdf747 | ||
|
|
c00b9b1549 | ||
|
|
2d88d488f4 | ||
|
|
72c9cfc893 | ||
|
|
0b619bdc08 | ||
|
|
db23e984da | ||
|
|
f0c8f39986 | ||
|
|
5d8ec1995a | ||
|
|
75d707e960 | ||
|
|
89b053b7bf | ||
|
|
e90aded6d3 | ||
|
|
8456ecd237 | ||
|
|
b89fdef1d9 | ||
|
|
6f3d576602 | ||
|
|
b026ec151e | ||
|
|
5d4c3877b1 | ||
|
|
113c5ab965 | ||
|
|
bce7993a6a | ||
|
|
3bbf416c20 | ||
|
|
b62b3b0dc3 | ||
|
|
7e72474b0a | ||
|
|
b05081328e | ||
|
|
0e7d1a7a50 | ||
|
|
2aaa73bb2a | ||
|
|
9000d90d72 | ||
|
|
4afcfd3846 | ||
|
|
6ba8265686 | ||
|
|
21583528c0 | ||
|
|
b1bb48f5b5 | ||
|
|
88f5f6e8f8 | ||
|
|
5439db2cc3 | ||
|
|
208cb18ee0 | ||
|
|
5d19becdaf | ||
|
|
4cd3ecdf15 | ||
|
|
8e26f529f7 | ||
|
|
3cc636d6b7 | ||
|
|
14dc439c82 | ||
|
|
21887988b2 | ||
|
|
232a57eed4 | ||
|
|
d1fd70e436 | ||
|
|
1d73162df1 | ||
|
|
7788cf4b44 | ||
|
|
d9dcddbdbb | ||
|
|
5fb616310d | ||
|
|
5a9cf7a22d | ||
|
|
d649f79ca4 | ||
|
|
ffce659332 | ||
|
|
24b30bf669 | ||
|
|
1bd9137761 | ||
|
|
f895c5a104 | ||
|
|
d0074709aa | ||
|
|
982eb6f49b | ||
|
|
b1ee085de4 | ||
|
|
2c66a44910 | ||
|
|
b95714a71e | ||
|
|
6183d811f1 | ||
|
|
6a2ed287ba | ||
|
|
d342728b46 | ||
|
|
f121445f24 | ||
|
|
124871da76 | ||
|
|
2062094228 | ||
|
|
515c9713e9 | ||
|
|
14c5d9e412 | ||
|
|
8e9429e5d0 | ||
|
|
6cb125001f | ||
|
|
1e7cf68317 | ||
|
|
2f6f6fb026 | ||
|
|
4618a392e2 | ||
|
|
cdcbd50fe7 | ||
|
|
8260e69dfe | ||
|
|
cd8ba740de | ||
|
|
8173b8f9d6 | ||
|
|
7d5b91fbe9 | ||
|
|
f9e50eba70 | ||
|
|
29e17f1ac8 | ||
|
|
ae8bf68d73 | ||
|
|
af146c507e | ||
|
|
7ad7a2c8ba | ||
|
|
8885a66883 | ||
|
|
2b40471e08 | ||
|
|
22253caf2d | ||
|
|
f8387180eb | ||
|
|
288643f0da | ||
|
|
0662dbbb01 | ||
|
|
1a8934f88b | ||
|
|
4cac361762 | ||
|
|
515cabb5cf | ||
|
|
5efa6176bb | ||
|
|
3515a483ca | ||
|
|
ca45df3c62 | ||
|
|
c94a96c47e | ||
|
|
745d8b0c4d | ||
|
|
ddc4d3bc90 | ||
|
|
906d71b35f | ||
|
|
beddb78469 | ||
|
|
fb968502ba | ||
|
|
643ce34cff | ||
|
|
cc08fb3d08 | ||
|
|
f3bab3177d | ||
|
|
e79302c915 | ||
|
|
d2f55bf0be | ||
|
|
c0208de111 | ||
|
|
56cd297ca3 | ||
|
|
5594525893 | ||
|
|
f91613b5d2 | ||
|
|
78e7e3fedc | ||
|
|
df017a349a | ||
|
|
8124fcbcc3 | ||
|
|
8658db537d | ||
|
|
bd382243a0 | ||
|
|
d0057f1873 | ||
|
|
342a93caa4 | ||
|
|
0904a4ade7 | ||
|
|
bbd525eeb5 | ||
|
|
352703a91f | ||
|
|
5a439f7ba8 | ||
|
|
f7200b0704 | ||
|
|
14bf8261da | ||
|
|
feb87d5c4e | ||
|
|
24320f22a6 | ||
|
|
98cf218cc3 | ||
|
|
7abe73e7bd | ||
|
|
a05ddacff3 | ||
|
|
20b18de60a | ||
|
|
d5c98dd62f | ||
|
|
61d08fa05a | ||
|
|
1ea2e713d8 | ||
|
|
38dce57666 | ||
|
|
8f255b8bde | ||
|
|
6b97d6668b | ||
|
|
3fa1d70379 | ||
|
|
2dd05a7aa6 | ||
|
|
02227edc23 | ||
|
|
a2297a84ad | ||
|
|
05cbd8adc7 | ||
|
|
984731b111 | ||
|
|
0da03c18d1 | ||
|
|
302b8fc8cd | ||
|
|
59239635cd | ||
|
|
238105a7da | ||
|
|
d7ca5f7e68 | ||
|
|
827135a02d | ||
|
|
a3ddf53207 | ||
|
|
35ac9015ad | ||
|
|
1c196ef675 | ||
|
|
2ce3b5a9b3 | ||
|
|
7421fd058d | ||
|
|
368e9df551 | ||
|
|
e9629046cb | ||
|
|
b5ccc419f6 | ||
|
|
615723da2a | ||
|
|
d1bcd5beff | ||
|
|
519dc583a0 | ||
|
|
0bf883cf56 | ||
|
|
c8502c735c | ||
|
|
48af97fbdd | ||
|
|
13bcf87ea5 | ||
|
|
e271fbb1e1 | ||
|
|
5678686c57 | ||
|
|
9fc3fd1aa5 | ||
|
|
e3a9c18cb3 | ||
|
|
9534b041db | ||
|
|
9426fa40a9 | ||
|
|
51e2b23082 | ||
|
|
aece914fb8 | ||
|
|
762e312730 | ||
|
|
e9eb529d6e | ||
|
|
e39a2b7dda | ||
|
|
cdc6baa4c0 | ||
|
|
255ab8dbf3 | ||
|
|
a76dc42bd8 | ||
|
|
f0cbd71129 | ||
|
|
aa10c3e093 | ||
|
|
0d836121d0 | ||
|
|
6e8ecbf5e5 | ||
|
|
8088ff197e | ||
|
|
55ee967f2f | ||
|
|
a56bd614bd | ||
|
|
992e8470b2 | ||
|
|
fe2bd8493f | ||
|
|
e43820c5ce | ||
|
|
662e30cb93 | ||
|
|
a51675e58d | ||
|
|
820b1dc643 | ||
|
|
94f2ae81c3 | ||
|
|
9020100313 | ||
|
|
d097940692 | ||
|
|
28dfcc8aec | ||
|
|
7dd0c8e7a4 | ||
|
|
bed21ad5c0 | ||
|
|
2622140529 | ||
|
|
3f9dc7a270 | ||
|
|
b5633c7fab | ||
|
|
5d053fedf4 | ||
|
|
d924694602 | ||
|
|
cf8a48ae0d | ||
|
|
24e865aa07 | ||
|
|
5d0dbbbe92 | ||
|
|
f2277e35b4 | ||
|
|
e8a642c5e2 | ||
|
|
399bdea220 | ||
|
|
9a0205758b | ||
|
|
0507e1af9c | ||
|
|
f893744ed1 | ||
|
|
e455e89777 | ||
|
|
9b60da96b0 | ||
|
|
34c0e7a0f4 | ||
|
|
550ce99bcd | ||
|
|
e81411c4a4 | ||
|
|
688a7e49db | ||
|
|
2ce997ca73 | ||
|
|
075967c395 | ||
|
|
1efd8bd25d | ||
|
|
e2bfd83481 | ||
|
|
2d5f0c69d0 | ||
|
|
339159a194 | ||
|
|
ad905e0bc5 | ||
|
|
721817515a | ||
|
|
73de357c9f | ||
|
|
b1e635d00b | ||
|
|
e489b5511a | ||
|
|
79409fb5c8 | ||
|
|
e9bfe12293 | ||
|
|
0833a7a0ca | ||
|
|
dfecab7c34 | ||
|
|
2eb57675c8 | ||
|
|
98f1454b3c | ||
|
|
d274768bcc | ||
|
|
ba31326302 | ||
|
|
f4e674fa9b | ||
|
|
1c7447b7e3 | ||
|
|
2d24c011aa | ||
|
|
b9b082b802 | ||
|
|
74dd9922a5 | ||
|
|
4298b63d91 | ||
|
|
c48fba7d8d | ||
|
|
7b71216a05 | ||
|
|
35204177e9 | ||
|
|
fc39278c91 | ||
|
|
eefd54137c | ||
|
|
880cd055dd | ||
|
|
ce07163541 | ||
|
|
6bd9acac6e | ||
|
|
1be14da122 | ||
|
|
96a61a46d0 | ||
|
|
4f4e43c953 | ||
|
|
306a35e294 | ||
|
|
53a0bdb2e3 | ||
|
|
bf73dcd1ea | ||
|
|
6409a9d442 | ||
|
|
96f96bd130 | ||
|
|
a8ce97c807 | ||
|
|
0804a69b4f | ||
|
|
e816b70f46 | ||
|
|
8adcd86cad | ||
|
|
9d2db45dd5 | ||
|
|
111276fb22 | ||
|
|
cad4623232 | ||
|
|
ececb58025 | ||
|
|
4a9ba3b7d4 | ||
|
|
b029adfb43 | ||
|
|
d0651474ab | ||
|
|
ca9db836de | ||
|
|
44ea97b938 | ||
|
|
84f5ab6c47 | ||
|
|
153372c2de | ||
|
|
7384b8922b | ||
|
|
295523b470 | ||
|
|
0991a24739 | ||
|
|
420c52b10e | ||
|
|
1e3ed210d7 | ||
|
|
947a75b771 | ||
|
|
c84ddad051 | ||
|
|
71b63ac6f9 | ||
|
|
659eb4ecfc | ||
|
|
2ff2c7db09 | ||
|
|
475e216ee0 | ||
|
|
226056fb46 | ||
|
|
580d18a41c | ||
|
|
0abd6c2a11 | ||
|
|
def43ecfc3 | ||
|
|
c4bfcf032a | ||
|
|
4a3a7d4812 | ||
|
|
dbe3e023ec | ||
|
|
c63110ae7d | ||
|
|
101f8beadb | ||
|
|
b16925b386 | ||
|
|
be5a146cca | ||
|
|
094fdcc51e | ||
|
|
307ad4a587 | ||
|
|
e867ba3c93 | ||
|
|
c3027c5901 | ||
|
|
30bf2d6e67 | ||
|
|
7fcab3bf31 | ||
|
|
eca8475e29 | ||
|
|
b340d46cdd | ||
|
|
ac721216ea | ||
|
|
e314d02212 | ||
|
|
6394e45bf2 | ||
|
|
ba5cc60c15 | ||
|
|
a0fed9956c | ||
|
|
58e9c97d76 | ||
|
|
ba154e4ec2 | ||
|
|
c4d7795280 | ||
|
|
25323d4ac1 | ||
|
|
15133cd0fc | ||
|
|
813554dc80 | ||
|
|
bd8980bc3c | ||
|
|
62a3713e1e | ||
|
|
1fb03de7be | ||
|
|
fa92a10eb7 | ||
|
|
bf20f247eb | ||
|
|
560b51e2f6 |
@@ -72,3 +72,16 @@ State:
|
||||
- assumptions;
|
||||
- findings not changed;
|
||||
- gaps or limitations.
|
||||
|
||||
## Domovoy Gen2 execution governance
|
||||
|
||||
When the active task is a Gen2 packet, read the canonical documents before editing:
|
||||
|
||||
- `docs/DOMOVOY_CONSTITUTION.md`
|
||||
- `architecture/GEN2_TARGET_ARCHITECTURE.md`
|
||||
- `architecture/GEN2_CANONICAL_DATA_MODEL.md`
|
||||
- `development/GEN2_MIGRATION_MAP.md`
|
||||
- `development/GEN2_MASTER_PLAN.md`
|
||||
- `development/GEN2_EXECUTION_CONTRACT.md`
|
||||
|
||||
The active task packet and `development/GEN2_TASK_INDEX.md` define execution order. `PRE-GEN2-000` freezes the V1 baseline; `ARCH-GEN2-000` accepts Gen2 governance; later `GEN2-*` packets must not be started automatically. Gen2 architecture and ownership changes are permitted only within the active packet's declared scope and authority. Default validation is FAST; MILESTONE or FULL is required only when the selected packet explicitly says so.
|
||||
|
||||
@@ -122,10 +122,10 @@ type compatibleChatResponse struct {
|
||||
}
|
||||
|
||||
func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" || len(request.Context) != 0 {
|
||||
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
|
||||
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
|
||||
}
|
||||
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
|
||||
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
|
||||
var result compatibleChatResponse
|
||||
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
|
||||
if err != nil {
|
||||
@@ -151,6 +151,19 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func localAdvisoryMessage(request cognitive.AdvisoryRequest) string {
|
||||
if len(request.Context) == 0 {
|
||||
return request.Message
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.WriteString(request.Message)
|
||||
builder.WriteString("\n\nRelevant local context:\n")
|
||||
for _, excerpt := range request.Context {
|
||||
fmt.Fprintf(&builder, "- [%s/%s] %s\n", excerpt.SourceKind, excerpt.SourceID, excerpt.Text)
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (a *OpenAICompatibleLocalAdapter) requestJSON(ctx context.Context, method, path string, input, output any) (int, error) {
|
||||
var body io.Reader
|
||||
if input != nil {
|
||||
|
||||
@@ -92,3 +92,32 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
|
||||
t.Fatal("expected bounded advisory to disable template thinking")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterPassesLocalContextToAdvisory(t *testing.T) {
|
||||
transport := &captureRoundTripper{}
|
||||
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1, RequestID: "request-context", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope",
|
||||
Message: "Продолжи разговор", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1,
|
||||
ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("local-model"),
|
||||
Context: []cognitive.ContextExcerpt{{ID: "focus", SourceKind: "situation", SourceID: "focus-1", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "Активный проект: Домовой"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload struct {
|
||||
Messages []struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
if err := json.Unmarshal(transport.body, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(payload.Messages) != 2 || !strings.Contains(payload.Messages[1].Content, "Активный проект: Домовой") {
|
||||
t.Fatalf("local context was not passed to advisory: %+v", payload.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
package multimodal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrEmptyResult = errors.New("multimodal provider returned an empty result")
|
||||
|
||||
type SpeechClient struct {
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type VisionClient struct {
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type ImageGenerationClient struct {
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type VideoUnderstandingClient struct {
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type GeneratedImage struct {
|
||||
DataBase64 string
|
||||
SourceURL string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type ImageGenerator interface {
|
||||
Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error)
|
||||
}
|
||||
|
||||
// ComfyImageGenerationClient is the replaceable transport for a ComfyUI
|
||||
// image-generation skill. The workflow is deliberately kept in this adapter:
|
||||
// the core only selects a capability and supplies the user's prompt.
|
||||
type ComfyImageGenerationClient struct {
|
||||
BaseURL string
|
||||
Client *http.Client
|
||||
PollInterval time.Duration
|
||||
PollTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c SpeechClient) Transcribe(ctx context.Context, filename, contentType string, data []byte) (string, error) {
|
||||
if len(data) == 0 {
|
||||
return "", errors.New("audio payload is empty")
|
||||
}
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
|
||||
header.Set("Content-Type", contentType)
|
||||
part, err := writer.CreatePart(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := writer.WriteField("response_format", "json"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/inference", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("speech provider status %d", response.StatusCode)
|
||||
}
|
||||
var result struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(result.Text) == "" {
|
||||
return "", ErrEmptyResult
|
||||
}
|
||||
return strings.TrimSpace(result.Text), nil
|
||||
}
|
||||
|
||||
func (c VisionClient) Describe(ctx context.Context, model, contentType string, data []byte, instruction string) (string, error) {
|
||||
if len(data) == 0 {
|
||||
return "", errors.New("image payload is empty")
|
||||
}
|
||||
payload := struct {
|
||||
Model string `json:"model"`
|
||||
Messages []struct {
|
||||
Role string `json:"role"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL *struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"image_url,omitempty"`
|
||||
} `json:"content"`
|
||||
} `json:"messages"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
}{}
|
||||
payload.Model = model
|
||||
payload.MaxTokens = 1024
|
||||
payload.Messages = append(payload.Messages, struct {
|
||||
Role string `json:"role"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL *struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"image_url,omitempty"`
|
||||
} `json:"content"`
|
||||
}{Role: "user", Content: []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL *struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"image_url,omitempty"`
|
||||
}{
|
||||
{Type: "text", Text: instruction},
|
||||
{Type: "image_url", ImageURL: &struct {
|
||||
URL string `json:"url"`
|
||||
}{URL: "data:" + contentType + ";base64," + encodeBase64(data)},
|
||||
}}})
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/chat/completions", bytes.NewReader(encoded))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("vision provider status %d", response.StatusCode)
|
||||
}
|
||||
var result struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(result.Choices) == 0 {
|
||||
return "", ErrEmptyResult
|
||||
}
|
||||
text := strings.TrimSpace(result.Choices[0].Message.Content)
|
||||
if text == "" {
|
||||
text = strings.TrimSpace(result.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
if text == "" {
|
||||
return "", ErrEmptyResult
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// Generate uses an OpenAI-compatible image-generation skill. The provider is
|
||||
// selected outside the core by the capability binding; this adapter contains
|
||||
// transport only and never decides what the user meant.
|
||||
func (c ImageGenerationClient) Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error) {
|
||||
if strings.TrimSpace(prompt) == "" {
|
||||
return GeneratedImage{}, errors.New("image prompt is empty")
|
||||
}
|
||||
payload := struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Size string `json:"size,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
}{Model: model, Prompt: prompt, Size: size, ResponseFormat: "b64_json"}
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/images/generations", bytes.NewReader(encoded))
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||||
return GeneratedImage{}, fmt.Errorf("image provider status %d", response.StatusCode)
|
||||
}
|
||||
var result struct {
|
||||
Data []struct {
|
||||
Base64 string `json:"b64_json"`
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
if len(result.Data) == 0 || (strings.TrimSpace(result.Data[0].Base64) == "" && strings.TrimSpace(result.Data[0].URL) == "") {
|
||||
return GeneratedImage{}, ErrEmptyResult
|
||||
}
|
||||
return GeneratedImage{DataBase64: result.Data[0].Base64, SourceURL: result.Data[0].URL, ContentType: "image/png"}, nil
|
||||
}
|
||||
|
||||
func (c ComfyImageGenerationClient) Generate(ctx context.Context, model, prompt, size string) (GeneratedImage, error) {
|
||||
if strings.TrimSpace(prompt) == "" {
|
||||
return GeneratedImage{}, errors.New("image prompt is empty")
|
||||
}
|
||||
width, height := imageSize(size)
|
||||
workflow := map[string]any{
|
||||
"3": map[string]any{"inputs": map[string]any{"seed": time.Now().UnixNano(), "steps": 8, "cfg": 1.0, "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0, "model": []any{"16", 0}, "positive": []any{"6", 0}, "negative": []any{"7", 0}, "latent_image": []any{"13", 0}}, "class_type": "KSampler"},
|
||||
"6": map[string]any{"inputs": map[string]any{"text": prompt, "clip": []any{"18", 0}}, "class_type": "CLIPTextEncode"},
|
||||
"7": map[string]any{"inputs": map[string]any{"text": "blurry, ugly, distorted", "clip": []any{"18", 0}}, "class_type": "CLIPTextEncode"},
|
||||
"8": map[string]any{"inputs": map[string]any{"samples": []any{"3", 0}, "vae": []any{"17", 0}}, "class_type": "VAEDecode"},
|
||||
"9": map[string]any{"inputs": map[string]any{"filename_prefix": "domovoy", "images": []any{"8", 0}}, "class_type": "SaveImage"},
|
||||
"13": map[string]any{"inputs": map[string]any{"width": width, "height": height, "batch_size": 1}, "class_type": "EmptySD3LatentImage"},
|
||||
"16": map[string]any{"inputs": map[string]any{"unet_name": "z_image_turbo_bf16.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
|
||||
"17": map[string]any{"inputs": map[string]any{"vae_name": "ae.safetensors"}, "class_type": "VAELoader"},
|
||||
"18": map[string]any{"inputs": map[string]any{"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}, "class_type": "CLIPLoader"},
|
||||
}
|
||||
payload := map[string]any{"prompt": workflow, "client_id": fmt.Sprintf("domovoy-%d", time.Now().UnixNano())}
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/prompt", bytes.NewReader(encoded))
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||||
return GeneratedImage{}, fmt.Errorf("comfy provider status %d", response.StatusCode)
|
||||
}
|
||||
var accepted struct {
|
||||
PromptID string `json:"prompt_id"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&accepted); err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
if accepted.PromptID == "" {
|
||||
return GeneratedImage{}, ErrEmptyResult
|
||||
}
|
||||
// A caller timeout must remove only this prompt. ComfyUI's global
|
||||
// /interrupt endpoint can otherwise stop unrelated users' work and leave
|
||||
// queued requests in an ambiguous state.
|
||||
defer func() {
|
||||
if ctx.Err() != nil {
|
||||
cancelCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := c.cancelPrompt(cancelCtx, accepted.PromptID); err != nil {
|
||||
// The original context error is more useful to the caller; the
|
||||
// provider cleanup failure remains observable in the server log.
|
||||
log.Printf("comfy prompt cleanup failed for %s: %v", accepted.PromptID, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
interval := c.PollInterval
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
}
|
||||
timeout := c.PollTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Minute
|
||||
}
|
||||
deadline := time.NewTimer(timeout)
|
||||
defer deadline.Stop()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
image, done, err := c.historyImage(ctx, accepted.PromptID)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, err
|
||||
}
|
||||
if done {
|
||||
return image, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return GeneratedImage{}, ctx.Err()
|
||||
case <-deadline.C:
|
||||
return GeneratedImage{}, context.DeadlineExceeded
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c ComfyImageGenerationClient) cancelPrompt(ctx context.Context, promptID string) error {
|
||||
if strings.TrimSpace(promptID) == "" {
|
||||
return errors.New("comfy prompt id is empty")
|
||||
}
|
||||
payload, err := json.Marshal(map[string][]string{"delete": {promptID}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/queue", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||||
return fmt.Errorf("comfy prompt cleanup status %d", response.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ComfyImageGenerationClient) historyImage(ctx context.Context, promptID string) (GeneratedImage, bool, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.BaseURL, "/")+"/history/"+url.PathEscape(promptID), nil)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, false, err
|
||||
}
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, false, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return GeneratedImage{}, false, nil
|
||||
}
|
||||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||||
return GeneratedImage{}, false, fmt.Errorf("comfy history status %d", response.StatusCode)
|
||||
}
|
||||
var history map[string]struct {
|
||||
Outputs map[string]struct {
|
||||
Images []struct {
|
||||
Filename string `json:"filename"`
|
||||
Subfolder string `json:"subfolder"`
|
||||
Type string `json:"type"`
|
||||
} `json:"images"`
|
||||
} `json:"outputs"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 4<<20)).Decode(&history); err != nil {
|
||||
return GeneratedImage{}, false, err
|
||||
}
|
||||
record, ok := history[promptID]
|
||||
if !ok {
|
||||
return GeneratedImage{}, false, nil
|
||||
}
|
||||
for _, output := range record.Outputs {
|
||||
for _, image := range output.Images {
|
||||
if image.Filename == "" {
|
||||
continue
|
||||
}
|
||||
query := url.Values{"filename": {image.Filename}, "subfolder": {image.Subfolder}, "type": {image.Type}}
|
||||
imageRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.BaseURL, "/")+"/view?"+query.Encode(), nil)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, false, err
|
||||
}
|
||||
imageResponse, err := c.client().Do(imageRequest)
|
||||
if err != nil {
|
||||
return GeneratedImage{}, false, err
|
||||
}
|
||||
data, readErr := io.ReadAll(io.LimitReader(imageResponse.Body, 32<<20))
|
||||
imageResponse.Body.Close()
|
||||
if readErr != nil {
|
||||
return GeneratedImage{}, false, readErr
|
||||
}
|
||||
if imageResponse.StatusCode < 200 || imageResponse.StatusCode > 299 || len(data) == 0 {
|
||||
return GeneratedImage{}, false, ErrEmptyResult
|
||||
}
|
||||
contentType := imageResponse.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "image/png"
|
||||
}
|
||||
return GeneratedImage{DataBase64: encodeBase64(data), ContentType: contentType}, true, nil
|
||||
}
|
||||
}
|
||||
return GeneratedImage{}, true, ErrEmptyResult
|
||||
}
|
||||
|
||||
func imageSize(size string) (int, int) {
|
||||
parts := strings.Split(strings.TrimSpace(size), "x")
|
||||
if len(parts) == 2 {
|
||||
var width, height int
|
||||
if _, err := fmt.Sscanf(size, "%dx%d", &width, &height); err == nil && width >= 256 && height >= 256 && width <= 2048 && height <= 2048 {
|
||||
return width, height
|
||||
}
|
||||
}
|
||||
return 512, 512
|
||||
}
|
||||
|
||||
// Describe accepts a video skill endpoint that performs its own bounded frame
|
||||
// sampling. Frame extraction is provider-owned so the core does not acquire a
|
||||
// media-processing dependency or hide a policy in orchestration code.
|
||||
func (c VideoUnderstandingClient) Describe(ctx context.Context, model, filename, contentType string, data []byte, instruction string) (string, error) {
|
||||
if len(data) == 0 {
|
||||
return "", errors.New("video payload is empty")
|
||||
}
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
|
||||
header.Set("Content-Type", contentType)
|
||||
part, err := writer.CreatePart(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = writer.WriteField("model", model)
|
||||
_ = writer.WriteField("instruction", instruction)
|
||||
if err := writer.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(c.BaseURL, "/")+"/understand", body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
response, err := c.client().Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||||
return "", fmt.Errorf("video provider status %d", response.StatusCode)
|
||||
}
|
||||
var result struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(result.Text) == "" {
|
||||
return "", ErrEmptyResult
|
||||
}
|
||||
return strings.TrimSpace(result.Text), nil
|
||||
}
|
||||
|
||||
func (c SpeechClient) client() *http.Client {
|
||||
if c.Client != nil {
|
||||
return c.Client
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (c VisionClient) client() *http.Client {
|
||||
if c.Client != nil {
|
||||
return c.Client
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (c ImageGenerationClient) client() *http.Client {
|
||||
if c.Client != nil {
|
||||
return c.Client
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (c ComfyImageGenerationClient) client() *http.Client {
|
||||
if c.Client != nil {
|
||||
return c.Client
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func (c VideoUnderstandingClient) client() *http.Client {
|
||||
if c.Client != nil {
|
||||
return c.Client
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
func encodeBase64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package multimodal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSpeechClientSendsMultipartAndReturnsTranscript(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/inference" || r.Method != http.MethodPost {
|
||||
t.Fatalf("unexpected speech request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
data, _ := io.ReadAll(file)
|
||||
if string(data) != "audio" {
|
||||
t.Fatalf("audio payload = %q", data)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"text": "распознанная фраза"})
|
||||
}))
|
||||
defer server.Close()
|
||||
text, err := (SpeechClient{BaseURL: server.URL, Client: server.Client()}).Transcribe(context.Background(), "voice.wav", "audio/wav", []byte("audio"))
|
||||
if err != nil || text != "распознанная фраза" {
|
||||
t.Fatalf("transcript=%q err=%v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisionClientSendsImageDataAndInstruction(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Messages []struct {
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
ImageURL *struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"image_url"`
|
||||
} `json:"content"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(payload.Messages) != 1 || len(payload.Messages[0].Content) != 2 || !strings.Contains(payload.Messages[0].Content[0].Text, "Опиши") || payload.Messages[0].Content[1].ImageURL == nil || !strings.HasPrefix(payload.Messages[0].Content[1].ImageURL.URL, "data:image/png;base64,") {
|
||||
t.Fatalf("unexpected vision payload: %+v", payload)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{"message": map[string]string{"content": "на изображении круг"}}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
text, err := (VisionClient{BaseURL: server.URL + "/v1", Client: server.Client()}).Describe(context.Background(), "vision-model", "image/png", []byte{0, 1, 2}, "Опиши изображение")
|
||||
if err != nil || text != "на изображении круг" {
|
||||
t.Fatalf("description=%q err=%v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisionClientUsesReasoningContentWhenProviderOmitsContent(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{
|
||||
"message": map[string]string{"content": "", "reasoning_content": "чёрный квадрат"},
|
||||
}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
text, err := (VisionClient{BaseURL: server.URL, Client: server.Client()}).Describe(context.Background(), "vision-model", "image/png", []byte("image"), "Опиши изображение")
|
||||
if err != nil || text != "чёрный квадрат" {
|
||||
t.Fatalf("description=%q err=%v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGenerationClientSendsPromptAndReturnsBase64(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/images/generations" || r.Method != http.MethodPost {
|
||||
t.Fatalf("unexpected image request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var payload struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.Model != "image-model" || payload.Prompt != "green bird" || payload.Size != "1024x1024" {
|
||||
t.Fatalf("unexpected image payload: %+v", payload)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]string{"b64_json": "aW1hZ2U="}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
image, err := (ImageGenerationClient{BaseURL: server.URL, Client: server.Client()}).Generate(context.Background(), "image-model", "green bird", "1024x1024")
|
||||
if err != nil || image.DataBase64 != "aW1hZ2U=" || image.ContentType != "image/png" {
|
||||
t.Fatalf("image=%+v err=%v", image, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoUnderstandingClientSendsMultipartAndReturnsDescription(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/understand" || r.Method != http.MethodPost {
|
||||
t.Fatalf("unexpected video request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.FormValue("model") != "video-model" || r.FormValue("instruction") != "describe movement" {
|
||||
t.Fatalf("unexpected video fields: model=%q instruction=%q", r.FormValue("model"), r.FormValue("instruction"))
|
||||
}
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
data, _ := io.ReadAll(file)
|
||||
if string(data) != "video" {
|
||||
t.Fatalf("video payload=%q", data)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"text": "человек идёт"})
|
||||
}))
|
||||
defer server.Close()
|
||||
text, err := (VideoUnderstandingClient{BaseURL: server.URL, Client: server.Client()}).Describe(context.Background(), "video-model", "clip.mp4", "video/mp4", []byte("video"), "describe movement")
|
||||
if err != nil || text != "человек идёт" {
|
||||
t.Fatalf("description=%q err=%v", text, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComfyImageGenerationClientRunsWorkflowAndReturnsImage(t *testing.T) {
|
||||
polls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/prompt":
|
||||
var payload struct {
|
||||
Prompt map[string]any `json:"prompt"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.Prompt["6"].(map[string]any)["inputs"].(map[string]any)["text"] != "red bird" {
|
||||
t.Fatalf("prompt was not forwarded: %+v", payload.Prompt["6"])
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"prompt_id": "test-prompt"})
|
||||
case "/history/test-prompt":
|
||||
polls++
|
||||
if polls == 1 {
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"test-prompt": map[string]any{"outputs": map[string]any{"9": map[string]any{"images": []any{map[string]string{"filename": "out.png", "subfolder": "", "type": "output"}}}}}})
|
||||
case "/view":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
_, _ = w.Write([]byte("png"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
image, err := (ComfyImageGenerationClient{BaseURL: server.URL, Client: server.Client(), PollInterval: time.Millisecond, PollTimeout: time.Second}).Generate(context.Background(), "", "red bird", "256x256")
|
||||
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
|
||||
t.Fatalf("image=%+v err=%v", image, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComfyImageGenerationClientDeletesOnlyCancelledPrompt(t *testing.T) {
|
||||
var deleted []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/prompt":
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"prompt_id": "cancel-me"})
|
||||
case "/history/cancel-me":
|
||||
<-r.Context().Done()
|
||||
case "/queue":
|
||||
var payload struct {
|
||||
Delete []string `json:"delete"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deleted = append(deleted, payload.Delete...)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := (ComfyImageGenerationClient{BaseURL: server.URL, Client: server.Client(), PollInterval: time.Millisecond, PollTimeout: time.Minute}).Generate(ctx, "", "red bird", "256x256")
|
||||
done <- err
|
||||
}()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
if err := <-done; !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
if len(deleted) != 1 || deleted[0] != "cancel-me" {
|
||||
t.Fatalf("deleted=%v", deleted)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package postgres
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -98,3 +99,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// RecentUtterancesForPrincipal provides durable conversational context when a
|
||||
// client starts a fresh conversation instead of carrying the previous ID.
|
||||
func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT utterance_id,conversation_id,actor_id,principal_id,channel,received_at_unix_millis,correlation_id,content,content_ref,speech_act,interpretation_state,corrects_utterance_id FROM interaction.utterances WHERE principal_id=$1 ORDER BY received_at_unix_millis DESC, utterance_id DESC LIMIT $2`, principalID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []interaction.HumanUtterance
|
||||
for rows.Next() {
|
||||
var u interaction.HumanUtterance
|
||||
if err := rows.Scan(&u.ID, &u.ConversationID, &u.ActorID, &u.PrincipalID, &u.Channel, &u.ReceivedAtUnixMillis, &u.CorrelationID, &u.Content, &u.ContentRef, &u.SpeechAct, &u.InterpretationState, &u.CorrectsUtteranceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, u)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/world"
|
||||
worlddomain "git.cin.su/m/Assistant/modules/world/domain"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type WorldRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
func OpenWorld(ctx context.Context, config ContinuityConfig) (*WorldRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WorldRepository{pool: pool}, nil
|
||||
}
|
||||
|
||||
func (r *WorldRepository) Close() { r.pool.Close() }
|
||||
|
||||
func (r *WorldRepository) SaveKindDefinition(ctx context.Context, value world.WorldObjectKindDefinition) error {
|
||||
if err := worlddomain.ValidateKindDefinition(value); err != nil {
|
||||
return err
|
||||
}
|
||||
var parentOwner, parentKind, parentID string
|
||||
var parentRevision *uint64
|
||||
if value.ParentKindRef != nil {
|
||||
parentOwner, parentKind, parentID = value.ParentKindRef.OwnerNamespace, value.ParentKindRef.SemanticKind, string(value.ParentKindRef.StableID)
|
||||
if value.ParentKindRef.Revision != nil {
|
||||
revision := uint64(*value.ParentKindRef.Revision)
|
||||
parentRevision = &revision
|
||||
}
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO world.kind_definitions(kind_definition_id,kind_key,revision,display_name,parent_owner_namespace,parent_semantic_kind,parent_stable_id,parent_revision,lifecycle,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(kind_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.DisplayName, parentOwner, parentKind, parentID, parentRevision, value.Lifecycle, value.DefinitionDigest, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorldRepository) SaveObject(ctx context.Context, value world.WorldObject) error {
|
||||
if err := worlddomain.ValidateObject(value); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO world.objects(world_object_id,principal_id,kind_owner_namespace,kind_semantic_kind,kind_stable_id,kind_revision,lifecycle,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(world_object_id) DO NOTHING`, value.ID, value.PrincipalID, value.KindDefinitionRef.OwnerNamespace, value.KindDefinitionRef.SemanticKind, value.KindDefinitionRef.StableID, value.KindRevision, value.Lifecycle, value.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorldRepository) SaveAlias(ctx context.Context, value world.WorldAlias) error {
|
||||
if err := worlddomain.ValidateAlias(value); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO world.aliases(world_alias_id,principal_id,world_object_id,alias,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(world_alias_id) DO NOTHING`, value.ID, value.PrincipalID, value.ObjectID, value.Alias, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorldRepository) SaveFacetDefinition(ctx context.Context, value world.FacetDefinition) error {
|
||||
if err := worlddomain.ValidateFacetDefinition(value); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.Marshal(value.ValueType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode facet value type: %w", err)
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO world.facet_definitions(facet_definition_id,kind_key,revision,facet_key,value_type,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(facet_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.FacetKey, payload, value.DefinitionDigest, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorldRepository) SaveRelationDefinition(ctx context.Context, value world.RelationDefinition) error {
|
||||
if err := worlddomain.ValidateRelationDefinition(value); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO world.relation_definitions(relation_definition_id,kind_key,revision,relation_key,target_kind_key,definition_digest,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(relation_definition_id) DO NOTHING`, value.ID, value.KindKey, value.Revision, value.RelationKey, value.TargetKindKey, value.DefinitionDigest, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorldRepository) SaveResolution(ctx context.Context, value world.IdentityResolutionDecisionRecord) error {
|
||||
if err := worlddomain.ValidateResolution(value); err != nil {
|
||||
return err
|
||||
}
|
||||
candidates, err := json.Marshal(value.Candidates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode resolution candidates: %w", err)
|
||||
}
|
||||
evidence, err := json.Marshal(value.Evidence)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode resolution evidence: %w", err)
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO world.identity_resolution_decisions(resolution_decision_id,principal_id,candidates,decision,evidence,confidence_millis,decided_at_unix_millis,revision) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(resolution_decision_id) DO NOTHING`, value.ID, value.PrincipalID, candidates, value.Decision, evidence, value.ConfidenceMillis, value.DecidedAt, value.Revision)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorldRepository) SaveLineage(ctx context.Context, value world.WorldLineage) error {
|
||||
if err := worlddomain.ValidateLineage(value); err != nil {
|
||||
return err
|
||||
}
|
||||
sources, err := json.Marshal(value.SourceRefs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode lineage sources: %w", err)
|
||||
}
|
||||
successors, err := json.Marshal(value.SuccessorRefs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode lineage successors: %w", err)
|
||||
}
|
||||
_, err = r.pool.Exec(ctx, `INSERT INTO world.lineage(lineage_id,operation,source_refs,successor_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT(lineage_id) DO NOTHING`, value.ID, value.Operation, sources, successors, value.RecordedAt)
|
||||
return err
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package research
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"html"
|
||||
@@ -19,9 +20,9 @@ import (
|
||||
var ErrSearchRejected = errors.New("web search request rejected")
|
||||
|
||||
var (
|
||||
searchLinkPattern = regexp.MustCompile(`(?is)<a[^>]+class=["'][^"']*result__a[^"']*["'][^>]*>(.*?)</a>`)
|
||||
searchLinkPattern = regexp.MustCompile(`(?is)(<a[^>]+class=["'][^"']*result__a[^"']*["'][^>]*>)(.*?)</a>`)
|
||||
searchSnippetPattern = regexp.MustCompile(`(?is)<a[^>]+class=["'][^"']*result__snippet[^"']*["'][^>]*>(.*?)</a>`)
|
||||
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*><a[^>]*>(.*?)</a>`)
|
||||
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*>(<a[^>]*>)(.*?)</a>`)
|
||||
bingSnippetPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<p[^>]*>(.*?)</p>`)
|
||||
tagPattern = regexp.MustCompile(`(?s)<[^>]*>`)
|
||||
termPattern = regexp.MustCompile(`(?i)[\p{L}\p{N}]{2,}`)
|
||||
@@ -36,19 +37,53 @@ var searchStopTerms = map[string]struct{}{
|
||||
// cities, weather terms, entities or model names: the complete semantic query
|
||||
// comes from the interpretation layer.
|
||||
type SearchConnector struct {
|
||||
Client *http.Client
|
||||
EndpointTemplate string
|
||||
AllowedHosts map[string]struct{}
|
||||
MaxBytes int64
|
||||
MaxResults int
|
||||
Client *http.Client
|
||||
EndpointTemplate string
|
||||
AllowedHosts map[string]struct{}
|
||||
FallbackEndpoints []SearchEndpoint
|
||||
MaxBytes int64
|
||||
MaxResults int
|
||||
}
|
||||
|
||||
// SearchEndpoint is an independently allowlisted web-search route. The
|
||||
// connector tries routes in order and stops at the first relevant evidence.
|
||||
// This is a source-recovery policy, not a question-type classifier.
|
||||
type SearchEndpoint struct {
|
||||
Template string
|
||||
AllowedHosts map[string]struct{}
|
||||
}
|
||||
|
||||
func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSearchRequest, nowUnixMillis int64) ([]epistemics.ResearchCandidate, error) {
|
||||
if request.Plan.Validate() != nil || strings.TrimSpace(request.Query) == "" || nowUnixMillis < 1 || !strings.Contains(c.EndpointTemplate, "%s") {
|
||||
return nil, ErrSearchRejected
|
||||
}
|
||||
endpoint, err := url.Parse(strings.Replace(c.EndpointTemplate, "%s", url.QueryEscape(strings.TrimSpace(request.Query)), 1))
|
||||
if err != nil || endpoint.Scheme != "https" || endpoint.Host == "" || !c.allowedHost(strings.ToLower(endpoint.Hostname())) {
|
||||
endpoints := make([]SearchEndpoint, 0, 1+len(c.FallbackEndpoints))
|
||||
endpoints = append(endpoints, SearchEndpoint{Template: c.EndpointTemplate, AllowedHosts: c.AllowedHosts})
|
||||
endpoints = append(endpoints, c.FallbackEndpoints...)
|
||||
var lastErr error
|
||||
for _, candidateEndpoint := range endpoints {
|
||||
results, err := c.collectEndpoint(ctx, candidateEndpoint, request, nowUnixMillis)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if len(results) > 0 {
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c SearchConnector) collectEndpoint(ctx context.Context, configured SearchEndpoint, request epistemics.WebSearchRequest, nowUnixMillis int64) ([]epistemics.ResearchCandidate, error) {
|
||||
if !strings.Contains(configured.Template, "%s") {
|
||||
return nil, ErrSearchRejected
|
||||
}
|
||||
searchQuery := enrichedSearchQuery(request.Query)
|
||||
endpoint, err := url.Parse(strings.Replace(configured.Template, "%s", url.QueryEscape(searchQuery), 1))
|
||||
if err != nil || endpoint.Scheme != "https" || endpoint.Host == "" || !allowedHost(configured.AllowedHosts, strings.ToLower(endpoint.Hostname())) {
|
||||
return nil, ErrSearchRejected
|
||||
}
|
||||
client := c.Client
|
||||
@@ -63,6 +98,10 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
|
||||
if err != nil {
|
||||
return nil, ErrSearchRejected
|
||||
}
|
||||
// Search providers commonly return a consent/challenge shell to the
|
||||
// default Go client. A neutral browser identification improves the same
|
||||
// generic evidence path for every query without selecting by topic.
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; DomovoyResearch/1.0)")
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -91,7 +130,8 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
|
||||
snapshotID := epistemics.SourceSnapshotID("snapshot:search:" + base)
|
||||
results := make([]epistemics.ResearchCandidate, 0, len(links))
|
||||
for index, link := range links {
|
||||
title := cleanHTML(string(link[1]))
|
||||
linkURL := resolveSearchResultLocator(extractHref(string(link[1])), endpoint)
|
||||
title := cleanHTML(string(link[2]))
|
||||
snippet := ""
|
||||
if index < len(snippets) {
|
||||
snippet = cleanHTML(string(snippets[index][1]))
|
||||
@@ -105,7 +145,7 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
|
||||
results = append(results, epistemics.ResearchCandidate{
|
||||
PropositionKey: request.PropositionKey + ":result:" + ordinal,
|
||||
Statement: statement,
|
||||
Source: epistemics.Source{ID: sourceID, Kind: "web-search", Locator: endpoint.String(), Title: title, Publisher: endpoint.Hostname()},
|
||||
Source: epistemics.Source{ID: sourceID, Kind: "web-search", Locator: linkURL, Title: title, Publisher: sourcePublisher(linkURL, endpoint.Hostname())},
|
||||
Snapshot: epistemics.SourceSnapshot{ID: snapshotID, SourceID: sourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent},
|
||||
Excerpt: epistemics.SourceExcerpt{ID: excerptID, SnapshotID: snapshotID, Text: statement, Start: 0, End: int64(len(statement))},
|
||||
Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
|
||||
@@ -120,6 +160,11 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
|
||||
func searchStatementRelatesToQuery(query, statement string) bool {
|
||||
terms := termPattern.FindAllString(strings.ToLower(query), -1)
|
||||
content := strings.ToLower(statement)
|
||||
productCue := containsSearchTerm(terms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор")
|
||||
if productCue && hasNumericSearchTerm(terms) {
|
||||
contentTerms := termPattern.FindAllString(content, -1)
|
||||
return hasNumericSearchTerm(contentTerms) && containsSearchTerm(contentTerms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор")
|
||||
}
|
||||
usable := 0
|
||||
for _, term := range terms {
|
||||
if _, stop := searchStopTerms[term]; stop || len([]rune(term)) < 4 {
|
||||
@@ -138,7 +183,38 @@ func searchStatementRelatesToQuery(query, statement string) bool {
|
||||
return usable == 0
|
||||
}
|
||||
|
||||
func (c SearchConnector) allowedHost(host string) bool { _, ok := c.AllowedHosts[host]; return ok }
|
||||
func containsSearchTerm(values []string, wanted ...string) bool {
|
||||
for _, value := range values {
|
||||
for _, candidate := range wanted {
|
||||
if strings.Contains(value, candidate) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasNumericSearchTerm(values []string) bool {
|
||||
for _, value := range values {
|
||||
for _, runeValue := range value {
|
||||
if runeValue >= '0' && runeValue <= '9' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func enrichedSearchQuery(query string) string {
|
||||
trimmed := strings.TrimSpace(query)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if (strings.Contains(lower, "цен") || strings.Contains(lower, "стоим")) && !strings.Contains(lower, "куп") {
|
||||
return trimmed + " цена купить сегодня"
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func allowedHost(hosts map[string]struct{}, host string) bool { _, ok := hosts[host]; return ok }
|
||||
func (c SearchConnector) limit() int {
|
||||
if c.MaxResults <= 0 || c.MaxResults > 8 {
|
||||
return 3
|
||||
@@ -148,3 +224,53 @@ func (c SearchConnector) limit() int {
|
||||
func cleanHTML(value string) string {
|
||||
return strings.Join(strings.Fields(html.UnescapeString(tagPattern.ReplaceAllString(value, ""))), " ")
|
||||
}
|
||||
|
||||
func extractHref(tag string) string {
|
||||
match := regexp.MustCompile(`(?is)\bhref=["']([^"']+)["']`).FindStringSubmatch(tag)
|
||||
if len(match) == 2 {
|
||||
return match[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveSearchResultLocator(raw string, searchEndpoint *url.URL) string {
|
||||
value := strings.TrimSpace(html.UnescapeString(raw))
|
||||
if value == "" {
|
||||
return searchEndpoint.String()
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return searchEndpoint.String()
|
||||
}
|
||||
if target := parsed.Query().Get("uddg"); target != "" {
|
||||
value = target
|
||||
parsed, err = url.Parse(target)
|
||||
if err != nil {
|
||||
return searchEndpoint.String()
|
||||
}
|
||||
}
|
||||
if target := parsed.Query().Get("u"); strings.HasPrefix(target, "a1") {
|
||||
if decoded, decodeErr := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(target, "a1")); decodeErr == nil {
|
||||
if targetURL, parseErr := url.Parse(string(decoded)); parseErr == nil {
|
||||
parsed = targetURL
|
||||
}
|
||||
}
|
||||
}
|
||||
if parsed.Scheme == "" {
|
||||
parsed.Scheme = "https"
|
||||
}
|
||||
if parsed.Host == "" && strings.HasPrefix(value, "//") {
|
||||
parsed, err = url.Parse("https:" + value)
|
||||
}
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
|
||||
return searchEndpoint.String()
|
||||
}
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func sourcePublisher(locator, fallback string) string {
|
||||
if parsed, err := url.Parse(locator); err == nil && parsed.Hostname() != "" {
|
||||
return parsed.Hostname()
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package research
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
func TestSearchConnectorFallsBackToAnotherAllowedSource(t *testing.T) {
|
||||
primary := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`<html><body>no usable results</body></html>`))
|
||||
}))
|
||||
defer primary.Close()
|
||||
fallback := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`<a class="result__a" href="https://example.test">Казань: текущая температура 18 °C</a><a class="result__snippet">Наблюдается переменная облачность.</a>`))
|
||||
}))
|
||||
defer fallback.Close()
|
||||
client := fallback.Client()
|
||||
client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // test servers only
|
||||
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan:test", Question: "какая погода в Казани", SourceClasses: []string{"web-search"}, FreshnessPolicy: string(epistemics.FreshnessCurrent)}, PropositionKey: "question:test", Query: "какая погода в Казани"}
|
||||
connector := SearchConnector{
|
||||
Client: client,
|
||||
EndpointTemplate: primary.URL + "?q=%s",
|
||||
AllowedHosts: map[string]struct{}{strings.TrimPrefix(primary.URL, "https://"): {}},
|
||||
FallbackEndpoints: []SearchEndpoint{{
|
||||
Template: fallback.URL + "?q=%s",
|
||||
AllowedHosts: map[string]struct{}{strings.TrimPrefix(fallback.URL, "https://"): {}},
|
||||
}},
|
||||
}
|
||||
// The allowlist is hostname-only; test servers include an ephemeral port.
|
||||
connector.AllowedHosts = map[string]struct{}{strings.Split(strings.TrimPrefix(primary.URL, "https://"), ":")[0]: {}}
|
||||
connector.FallbackEndpoints[0].AllowedHosts = map[string]struct{}{strings.Split(strings.TrimPrefix(fallback.URL, "https://"), ":")[0]: {}}
|
||||
results, err := connector.Collect(context.Background(), request, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(results) != 1 || !strings.Contains(results[0].Statement, "18") {
|
||||
t.Fatalf("fallback evidence = %#v", results)
|
||||
}
|
||||
if results[0].Source.Locator != "https://example.test" || results[0].Source.Publisher != "example.test" {
|
||||
t.Fatalf("search result source = %#v", results[0].Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichedSearchQueryAddsPriceSignalsOnlyToPriceRequests(t *testing.T) {
|
||||
if got := enrichedSearchQuery("проверь цену на видеокарту 4090"); got != "проверь цену на видеокарту 4090 цена купить сегодня" {
|
||||
t.Fatalf("price query = %q", got)
|
||||
}
|
||||
if got := enrichedSearchQuery("какая погода в Казани"); got != "какая погода в Казани" {
|
||||
t.Fatalf("non-price query = %q", got)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ The system already has the correct outbound mutation foundation: Commit owns aut
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No Gmail, MCP, Codex Bridge, browser automation, mobile app, voice, or self-extension loop.
|
||||
- No Gmail, MCP, Codex Bridge, mobile app, voice, or self-extension loop. Browser automation is allowed only under ADR-0019 as bounded read-only observation; it is not a human-surface command or mutation channel.
|
||||
- No new canonical owner.
|
||||
- No generic scheduler/event-bus/autonomous-agent loop.
|
||||
- No new third-party dependency.
|
||||
|
||||
@@ -30,7 +30,7 @@ Examples include: a reminder after notification, waiting work requiring a decisi
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No Codex Bridge, Gmail, MCP, voice, mobile, browser automation, self-extension loop, or generalized orchestration.
|
||||
- No Codex Bridge, Gmail, MCP, voice, mobile, self-extension loop, or generalized orchestration. Browser observation may be used only through the bounded read-only capability defined by ADR-0019 and cannot continue or mutate a response target.
|
||||
- No universal object ontology.
|
||||
- No reminder-specific parallel subsystem.
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR-0019 — Bounded Site-Browser Observation
|
||||
|
||||
Status: Accepted
|
||||
|
||||
Task: DEV-000293
|
||||
|
||||
## Context
|
||||
|
||||
Search snippets are insufficient for dynamic pages such as marketplaces: they may expose a category page while hiding the current product price behind client-side rendering. Domovoy needs a replaceable observation capability that can inspect an allowlisted rendered page without becoming an account agent, universal scraper, or external mutation path.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Browser automation is allowed only as a server-owned, read-only observation capability behind the existing Capability and research adapter boundaries.
|
||||
2. The browser runtime may navigate only to an explicit HTTPS host allowlist and bounded navigation policy. Browser-supplied or model-supplied authority, credentials, proxy settings, cookies, headers, download paths, or arbitrary JavaScript are rejected.
|
||||
3. The capability may load public pages and wait for bounded client-side rendering. It may read visible text and explicitly declared structured-data selectors, then return an Epistemics Observation with source URL, observed time, coverage, and failure state.
|
||||
4. Login, cookies, account state, CAPTCHA solving, checkout, form submission, uploads, downloads, purchases, messages, mutations, and external side effects are out of scope.
|
||||
5. A blocked page, challenge, timeout, partial DOM, missing price, or conflicting value remains `UNKNOWN`, `PARTIAL`, or `NOT_OBSERVED`; it never becomes a verified fact by model interpretation alone.
|
||||
6. Browser observation is optional. Search-only routing remains the fallback when the capability is unavailable, degraded, or not allowlisted for the requested host.
|
||||
7. Capability owns availability, host policy, health and limits. Epistemics owns the resulting observation and provenance. No new canonical owner or database schema is introduced.
|
||||
8. The first implementation may be development-only. Production activation requires independent capability health, allowlist evidence, adversarial navigation tests, and a live read-only smoke test.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Core/domain code does not import a browser or provider runtime.
|
||||
- The browser adapter performs only bounded GET/navigation observation and cannot reach Commit/Executor.
|
||||
- Secrets remain outside the browser runtime and its artifacts, logs, screenshots, page content and chat context.
|
||||
- An observation is evidence for answer synthesis, not an instruction to mutate a site or canonical state.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Dynamic public pages can provide stronger evidence than search snippets when the site permits it.
|
||||
- Site-specific selectors and host policy are replaceable adapter configuration, not semantic domain logic.
|
||||
- Browser unavailability is a capability gap, not a runtime-wide outage.
|
||||
- The old blanket prohibition on browser automation is replaced by this narrower, testable boundary.
|
||||
@@ -0,0 +1,45 @@
|
||||
# ADR-0020 — Architecture Generation 2 governance adoption
|
||||
|
||||
**Status:** Accepted by `ARCH-GEN2-000`
|
||||
**Date:** 2026-08-29
|
||||
**Scope:** Architecture and governance only; no runtime or schema migration.
|
||||
|
||||
## Decision
|
||||
|
||||
Assistant adopts the documents under `docs/DOMOVOY_CONSTITUTION.md`,
|
||||
`architecture/GEN2_TARGET_ARCHITECTURE.md`,
|
||||
`architecture/GEN2_CANONICAL_DATA_MODEL.md`,
|
||||
`development/GEN2_MIGRATION_MAP.md`,
|
||||
`development/GEN2_MASTER_PLAN.md`, and the execution contract as the
|
||||
canonical Architecture Generation 2 governance basis.
|
||||
|
||||
The existing V1 implementation remains the rollback and compatibility
|
||||
baseline. This decision authorizes subsequent, separately accepted Gen2
|
||||
packets to introduce the World, Configuration, and SELF bounded owners and
|
||||
Gen2 migrations, subject to explicit ownership, schema, contract, and
|
||||
rollback decisions in those packets. It does not create those owners or
|
||||
change canonical runtime state in this task.
|
||||
|
||||
## Authority and execution order
|
||||
|
||||
The Gen2 constitution and accepted architecture decisions govern the Gen2
|
||||
program. The target architecture, canonical data model, migration map,
|
||||
master plan, execution contract, task index, and individual task packet are
|
||||
interpreted in that order. A lower-level packet cannot silently weaken a
|
||||
higher-level invariant or reorder the task index.
|
||||
|
||||
Every Gen2 task starts from `gen2-integration`, uses the active task pointer,
|
||||
and passes the declared FAST/MILESTONE/FULL gate before merge. `ARCH-GEN2-000`
|
||||
is the architecture adoption root accepted against the PRE baseline;
|
||||
`GEN2-001` and later tasks are not accepted by this decision and require
|
||||
separate execution.
|
||||
|
||||
## Invariants preserved
|
||||
|
||||
- No new canonical owner, cross-module dependency, privilege, database role,
|
||||
third-party dependency, runtime writer, event, or migration is introduced.
|
||||
- Existing Authority → Commit → Executor boundaries remain mandatory.
|
||||
- Memory remains a rebuildable retrieval fabric, World is not a generic truth
|
||||
owner, and SELF is not a monolithic orchestrator.
|
||||
- The `domovoy-pre-gen2` baseline tag remains immutable and is the rollback
|
||||
point until a later migration task explicitly establishes another one.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"registry_version": 1,
|
||||
"semantic_generation": 1,
|
||||
"architecture_generation": 1,
|
||||
"architecture_generation": 2,
|
||||
"modules": {
|
||||
"identity": {
|
||||
"id": "mod.identity",
|
||||
@@ -179,6 +179,30 @@
|
||||
"database_schema": "interaction",
|
||||
"boundary_class": "remote_capable",
|
||||
"may_depend_on": ["contracts.identity", "contracts.situation", "contracts.epistemics", "contracts.cognitive", "platform.ids", "platform.events"]
|
||||
},
|
||||
"world": {
|
||||
"id": "mod.world",
|
||||
"owns": ["WorldObject", "WorldObjectKindDefinition", "WorldAlias", "FacetDefinition", "RelationDefinition", "IdentityResolutionCandidate", "IdentityResolutionDecision", "WorldLineage"],
|
||||
"database_schema": "world",
|
||||
"boundary_class": "remote_capable",
|
||||
"may_depend_on": ["contracts.identity", "platform.ids", "platform.events"],
|
||||
"forbidden": ["adapters.models", "adapters.temporal", "claims", "beliefs", "observations"]
|
||||
},
|
||||
"configuration": {
|
||||
"id": "mod.configuration",
|
||||
"owns": ["SettingDefinition", "SettingAssignment", "ConfigurationRevision", "ConfigurationChangeProvenance"],
|
||||
"database_schema": "configuration",
|
||||
"boundary_class": "remote_capable",
|
||||
"may_depend_on": ["contracts.identity", "contracts.authority", "platform.ids", "platform.events", "platform.secrets"],
|
||||
"forbidden": ["secret_values", "arbitrary_key_value_without_definition"]
|
||||
},
|
||||
"self": {
|
||||
"id": "mod.self",
|
||||
"owns": ["CompetenceAssessment", "WeaknessPattern", "DependencyAssessment", "LearningLedgerEntry", "ImprovementNeed", "SelfVersionView", "SelfEvaluationInterpretation"],
|
||||
"database_schema": "self",
|
||||
"boundary_class": "remote_capable",
|
||||
"may_depend_on": ["contracts.capability", "contracts.epistemics", "contracts.artifact", "platform.ids", "platform.events"],
|
||||
"forbidden": ["adapters.models", "tasks", "authority_grants", "code_release_switching"]
|
||||
}
|
||||
},
|
||||
"platform": {
|
||||
@@ -244,6 +268,26 @@
|
||||
"cross_owner_write": "forbidden",
|
||||
"cross_owner_application_join": "forbidden"
|
||||
},
|
||||
"runtime_topology_v2": {
|
||||
"core_runtime": {
|
||||
"modules": ["identity", "situation", "work", "epistemics", "capability", "artifact", "interaction", "world", "configuration", "self"]
|
||||
},
|
||||
"continuity_runtime": {"modules": ["continuity"]},
|
||||
"commit_runtime": {"modules": ["authority", "commit"]},
|
||||
"cognitive_workers": {"modules": [], "role": "replaceable reasoning/context workers; no canonical ownership"},
|
||||
"supervisor_runtime": {"modules": [], "role": "release health, switch, rollback and recovery; not a semantic owner"}
|
||||
},
|
||||
"storage_topology_v2": {
|
||||
"postgres_cluster": "assistant_primary",
|
||||
"schemas": {
|
||||
"identity": "identity_role", "authority": "authority_role", "situation": "situation_role", "work": "work_role",
|
||||
"epistemics": "epistemics_role", "continuity": "continuity_role", "capability": "capability_role", "commit": "commit_role",
|
||||
"artifact": "artifact_role", "interaction": "interaction_role", "world": "world_role", "configuration": "configuration_role", "self": "self_role"
|
||||
},
|
||||
"cross_owner_fk": "forbidden_by_default",
|
||||
"cross_owner_write": "forbidden",
|
||||
"cross_owner_application_join": "forbidden"
|
||||
},
|
||||
"invariants": [
|
||||
{
|
||||
"id": "INV-AUTH-001",
|
||||
@@ -309,6 +353,14 @@
|
||||
".gitea/workflows/ci.yml",
|
||||
"cmd/devctl/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"docs/DOMOVOY_CONSTITUTION.md",
|
||||
"docs/DOMOVOY_GEN2_GAP_MAP.md",
|
||||
"architecture/GEN2_TARGET_ARCHITECTURE.md",
|
||||
"architecture/GEN2_CANONICAL_DATA_MODEL.md",
|
||||
"development/GEN2_MIGRATION_MAP.md",
|
||||
"development/GEN2_MASTER_PLAN.md",
|
||||
"development/GEN2_EXECUTION_CONTRACT.md",
|
||||
"development/GEN2_TASK_INDEX.md",
|
||||
"security/**"
|
||||
],
|
||||
"default_feature_task_authority": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
registry_version: 1
|
||||
semantic_generation: 1
|
||||
architecture_generation: 1
|
||||
architecture_generation: 2
|
||||
|
||||
modules:
|
||||
identity:
|
||||
@@ -75,6 +75,30 @@ modules:
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.situation, contracts.epistemics, contracts.cognitive, platform.ids, platform.events]
|
||||
|
||||
world:
|
||||
id: mod.world
|
||||
owns: [WorldObject, WorldObjectKindDefinition, WorldAlias, FacetDefinition, RelationDefinition, IdentityResolutionCandidate, IdentityResolutionDecision, WorldLineage]
|
||||
database_schema: world
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, platform.ids, platform.events]
|
||||
forbidden: [adapters.models, adapters.temporal, claims, beliefs, observations]
|
||||
|
||||
configuration:
|
||||
id: mod.configuration
|
||||
owns: [SettingDefinition, SettingAssignment, ConfigurationRevision, ConfigurationChangeProvenance]
|
||||
database_schema: configuration
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.identity, contracts.authority, platform.ids, platform.events, platform.secrets]
|
||||
forbidden: [secret_values, arbitrary_key_value_without_definition]
|
||||
|
||||
self:
|
||||
id: mod.self
|
||||
owns: [CompetenceAssessment, WeaknessPattern, DependencyAssessment, LearningLedgerEntry, ImprovementNeed, SelfVersionView, SelfEvaluationInterpretation]
|
||||
database_schema: self
|
||||
boundary_class: remote_capable
|
||||
may_depend_on: [contracts.capability, contracts.epistemics, contracts.artifact, platform.ids, platform.events]
|
||||
forbidden: [adapters.models, tasks, authority_grants, code_release_switching]
|
||||
|
||||
platform:
|
||||
allowed_packages:
|
||||
- platform.ids
|
||||
@@ -98,6 +122,20 @@ runtime_topology_v1:
|
||||
modules: []
|
||||
role: "replaceable cognition/context compilation workers; no canonical ownership"
|
||||
|
||||
runtime_topology_v2:
|
||||
core_runtime:
|
||||
modules: [identity, situation, work, epistemics, capability, artifact, interaction, world, configuration, self]
|
||||
continuity_runtime:
|
||||
modules: [continuity]
|
||||
commit_runtime:
|
||||
modules: [authority, commit]
|
||||
cognitive_workers:
|
||||
modules: []
|
||||
role: "replaceable reasoning/context workers; no canonical ownership"
|
||||
supervisor_runtime:
|
||||
modules: []
|
||||
role: "release health, switch, rollback and recovery; not a semantic owner"
|
||||
|
||||
storage_topology_v1:
|
||||
postgres_cluster: assistant_primary
|
||||
schemas:
|
||||
@@ -115,6 +153,26 @@ storage_topology_v1:
|
||||
cross_owner_write: forbidden
|
||||
cross_owner_application_join: forbidden
|
||||
|
||||
storage_topology_v2:
|
||||
postgres_cluster: assistant_primary
|
||||
schemas:
|
||||
identity: identity_role
|
||||
authority: authority_role
|
||||
situation: situation_role
|
||||
work: work_role
|
||||
epistemics: epistemics_role
|
||||
continuity: continuity_role
|
||||
capability: capability_role
|
||||
commit: commit_role
|
||||
artifact: artifact_role
|
||||
interaction: interaction_role
|
||||
world: world_role
|
||||
configuration: configuration_role
|
||||
self: self_role
|
||||
cross_owner_fk: forbidden_by_default
|
||||
cross_owner_write: forbidden
|
||||
cross_owner_application_join: forbidden
|
||||
|
||||
invariants:
|
||||
- id: INV-AUTH-001
|
||||
property: "No external mutation without current valid authority and target/revision checks"
|
||||
@@ -149,6 +207,14 @@ protected_paths:
|
||||
- .gitea/workflows/ci.yml
|
||||
- cmd/devctl/**
|
||||
- dev/ACTIVE_TASK.json
|
||||
- docs/DOMOVOY_CONSTITUTION.md
|
||||
- docs/DOMOVOY_GEN2_GAP_MAP.md
|
||||
- architecture/GEN2_TARGET_ARCHITECTURE.md
|
||||
- architecture/GEN2_CANONICAL_DATA_MODEL.md
|
||||
- development/GEN2_MIGRATION_MAP.md
|
||||
- development/GEN2_MASTER_PLAN.md
|
||||
- development/GEN2_EXECUTION_CONTRACT.md
|
||||
- development/GEN2_TASK_INDEX.md
|
||||
- security/**
|
||||
|
||||
default_feature_task_authority:
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -31,35 +32,16 @@ type fleetStatusResponse struct {
|
||||
func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, endpoints []fleetEndpoint) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
members := make([]capability.FleetMember, 0, len(endpoints))
|
||||
for _, endpoint := range endpoints {
|
||||
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
|
||||
if strings.TrimSpace(endpoint.URL) == "" {
|
||||
members = append(members, member)
|
||||
continue
|
||||
}
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
|
||||
if err == nil {
|
||||
for name, value := range endpoint.Headers {
|
||||
request.Header.Set(name, value)
|
||||
}
|
||||
probeClient := endpoint.Client
|
||||
if probeClient == nil {
|
||||
probeClient = client
|
||||
}
|
||||
response, requestErr := probeClient.Do(request)
|
||||
if requestErr == nil {
|
||||
response.Body.Close()
|
||||
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
||||
member.Ready, member.Reason = true, "AVAILABLE"
|
||||
} else {
|
||||
member.Reason = fmt.Sprintf("status=%s", response.Status)
|
||||
}
|
||||
} else {
|
||||
member.Reason = requestErr.Error()
|
||||
}
|
||||
}
|
||||
members = append(members, member)
|
||||
members = make([]capability.FleetMember, len(endpoints))
|
||||
var waitGroup sync.WaitGroup
|
||||
for index, endpoint := range endpoints {
|
||||
waitGroup.Add(1)
|
||||
go func(index int, endpoint fleetEndpoint) {
|
||||
defer waitGroup.Done()
|
||||
members[index] = probeFleetEndpoint(r.Context(), client, endpoint)
|
||||
}(index, endpoint)
|
||||
}
|
||||
waitGroup.Wait()
|
||||
readiness, err := fleetdomain.BuildFleetReadiness(time.Now().UnixMilli(), members)
|
||||
response := fleetStatusResponse{Readiness: readiness, Preferred: preferred}
|
||||
if err == nil {
|
||||
@@ -76,4 +58,37 @@ func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier,
|
||||
})
|
||||
}
|
||||
|
||||
func probeFleetEndpoint(parent context.Context, fallbackClient *http.Client, endpoint fleetEndpoint) capability.FleetMember {
|
||||
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
|
||||
if strings.TrimSpace(endpoint.URL) == "" {
|
||||
return member
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.URL, nil)
|
||||
if err != nil {
|
||||
member.Reason = err.Error()
|
||||
return member
|
||||
}
|
||||
for name, value := range endpoint.Headers {
|
||||
request.Header.Set(name, value)
|
||||
}
|
||||
probeClient := endpoint.Client
|
||||
if probeClient == nil {
|
||||
probeClient = fallbackClient
|
||||
}
|
||||
response, requestErr := probeClient.Do(request)
|
||||
if requestErr != nil {
|
||||
member.Reason = requestErr.Error()
|
||||
return member
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
||||
member.Ready, member.Reason = true, "AVAILABLE"
|
||||
} else {
|
||||
member.Reason = fmt.Sprintf("status=%s", response.Status)
|
||||
}
|
||||
return member
|
||||
}
|
||||
|
||||
func fleetContext(ctx context.Context) context.Context { return ctx }
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
@@ -26,6 +27,29 @@ func TestFleetReadinessSelectsBestAvailablePrimaryTierBeforeLifeboat(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetReadinessDoesNotWaitForSlowMembersSerially(t *testing.T) {
|
||||
slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
defer slow.Close()
|
||||
fast := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
|
||||
defer fast.Close()
|
||||
handler := fleetReadinessHandler(fast.Client(), capability.FleetTierFast, []fleetEndpoint{
|
||||
{ProviderID: "slow", ModelID: "slow-model", Tier: capability.FleetTierMedium, URL: slow.URL},
|
||||
{ProviderID: "lifeboat", ModelID: "lifeboat-model", Tier: capability.FleetTierLifeboat, URL: fast.URL},
|
||||
})
|
||||
started := time.Now()
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"LIFEBOAT"`) {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed >= 300*time.Millisecond {
|
||||
t.Fatalf("fleet readiness was serialized: elapsed=%s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer fleet-key" {
|
||||
|
||||
@@ -186,27 +186,61 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string)
|
||||
}
|
||||
|
||||
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
|
||||
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
|
||||
if request.Meaning.SpeechAct == "RESPONSE_REVIEW" {
|
||||
const marker = "Черновик ответа:"
|
||||
if index := strings.LastIndex(request.Message, marker); index >= 0 {
|
||||
if draft := strings.TrimSpace(request.Message[index+len(marker):]); draft != "" {
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: draft, FinishReason: cognitive.FinishCompleted}
|
||||
}
|
||||
}
|
||||
}
|
||||
text := "Понял."
|
||||
identityText := request.Meaning.RequestedOutcome
|
||||
if identityText == "" {
|
||||
identityText = request.Message
|
||||
}
|
||||
if request.Meaning.SpeechAct == "QUESTION" && isIdentityInquiry(identityText) {
|
||||
text = "Я — Домовой, ваш помощник."
|
||||
}
|
||||
switch request.Meaning.InformationRequestKind {
|
||||
case interaction.InformationRequestCognitiveModelInventory:
|
||||
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
|
||||
text = "Сейчас не смог проверить подключённые модели."
|
||||
case interaction.InformationRequestCapabilityInventory:
|
||||
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
|
||||
text = "Сейчас не смог проверить доступные возможности."
|
||||
case interaction.InformationRequestCognitiveChannelStatus:
|
||||
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
|
||||
text = "Локальный канал Домового доступен; подробности состояния дам по запросу."
|
||||
case interaction.InformationRequestCognitiveRecovery:
|
||||
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
|
||||
text = "Встроенный канал сейчас недоступен."
|
||||
default:
|
||||
switch request.Meaning.SpeechAct {
|
||||
case "REQUEST_TO_INVESTIGATE":
|
||||
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
|
||||
text = "Понял задачу, но пока не смог получить проверяемый результат."
|
||||
case "QUESTION":
|
||||
text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально."
|
||||
if !isIdentityInquiry(identityText) {
|
||||
text = "Пока не смог получить проверяемый ответ."
|
||||
}
|
||||
case "CONVERSATIONAL":
|
||||
text = "Я — Домовой, ваш помощник."
|
||||
}
|
||||
}
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
|
||||
}
|
||||
|
||||
func isIdentityInquiry(message string) bool {
|
||||
tokens := strings.Fields(strings.ToLower(strings.NewReplacer("?", " ", "!", " ", ",", " ").Replace(message)))
|
||||
hasIdentitySubject := false
|
||||
hasAssistantReference := false
|
||||
for _, token := range tokens {
|
||||
switch token {
|
||||
case "кто", "зовут", "имя":
|
||||
hasIdentitySubject = true
|
||||
case "ты", "тебя", "тебе", "вы", "вас":
|
||||
hasAssistantReference = true
|
||||
}
|
||||
}
|
||||
return hasIdentitySubject && hasAssistantReference
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
|
||||
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
|
||||
@@ -185,11 +185,34 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
|
||||
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
|
||||
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
|
||||
response := builtinAdvisory(request)
|
||||
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
|
||||
if response.AdvisoryText != "Встроенный канал сейчас недоступен." {
|
||||
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInAdvisoryIdentityIsNaturalAndDoesNotMentionResponseLength(t *testing.T) {
|
||||
response := builtinAdvisory(cognitive.GatewayAdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: "request-identity",
|
||||
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION", RequestedOutcome: "ты кто?"},
|
||||
})
|
||||
if response.AdvisoryText != "Я — Домовой, ваш помощник." {
|
||||
t.Fatalf("unexpected identity response: %q", response.AdvisoryText)
|
||||
}
|
||||
if strings.Contains(response.AdvisoryText, "коротко") || strings.Contains(response.AdvisoryText, "подробност") {
|
||||
t.Fatalf("identity response contains style meta-text: %q", response.AdvisoryText)
|
||||
}
|
||||
messageResponse := builtinAdvisory(cognitive.GatewayAdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1,
|
||||
RequestID: "request-identity-message",
|
||||
Message: "как тебя зовут?",
|
||||
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION"},
|
||||
})
|
||||
if messageResponse.AdvisoryText != "Я — Домовой, ваш помощник." {
|
||||
t.Fatalf("unexpected message identity response: %q", messageResponse.AdvisoryText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
|
||||
|
||||
@@ -67,12 +67,18 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
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})
|
||||
// A selected provider can become unavailable after discovery. Semantic
|
||||
// routing must still retain the deterministic interpretation and use the
|
||||
// local lifeboat rather than turn an ordinary conversation into a
|
||||
// degraded reply solely because one provider failed.
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
|
||||
if !ok {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
|
||||
// Structured interpretation is a transport contract. A malformed model
|
||||
// answer is not a reason to discard the available local fallback.
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
@@ -24,3 +30,32 @@ func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T)
|
||||
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretationProviderFailureUsesLocalLifeboat(t *testing.T) {
|
||||
selected := gatewaySelection()
|
||||
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
|
||||
primary := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishProviderUnavailable}}
|
||||
lifeboat := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":true,"clarification":"NOT_REQUIRED","selection_reason":"local lifeboat","capability_hints":["COGNITIVE_REASONING"]}`, FinishReason: cognitive.FinishCompleted}}
|
||||
gateway := cognitiveGateway{
|
||||
resolver: ®istryStub{selected: selected},
|
||||
providers: map[capability.ProviderKind][]providerRegistration{
|
||||
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, Provider: primary}},
|
||||
},
|
||||
builtinLocal: &providerRegistration{Provider: lifeboat},
|
||||
now: time.Now,
|
||||
}
|
||||
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "сколько фигур в шахматах?", Locale: "ru", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, DeterministicSpeechAct: "QUESTION", DeterministicConfidence: 800, DeterministicApplicability: cognitive.InterpretationApplicable}
|
||||
payload, _ := json.Marshal(request)
|
||||
recorder := httptest.NewRecorder()
|
||||
gateway.interpretationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload)))
|
||||
if recorder.Code != http.StatusOK || !primary.called || !lifeboat.called {
|
||||
t.Fatalf("status=%d primary=%t lifeboat=%t body=%s", recorder.Code, primary.called, lifeboat.called, recorder.Body.String())
|
||||
}
|
||||
var response cognitive.GatewayInterpretationResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.FinishReason != cognitive.FinishCompleted || response.Candidate.SelectionReason != "local lifeboat" {
|
||||
t.Fatalf("response=%+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/adapters/multimodal"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
@@ -126,7 +127,9 @@ func main() {
|
||||
}
|
||||
cpuEndpoint := strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"
|
||||
cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(cpuReadinessClient, cpuEndpoint, cpuFleetHeaders)
|
||||
cpuHealthProbe.Timeout = 10 * time.Second
|
||||
// Opportunistic providers must not hide a ready local lifeboat. Keep their
|
||||
// readiness budget short; their detailed fleet state is reported separately.
|
||||
cpuHealthProbe.Timeout = 2 * time.Second
|
||||
mediumEndpoint := ""
|
||||
mediumHeaders := map[string]string{}
|
||||
mediumHealthProbe := health.Probe{}
|
||||
@@ -136,7 +139,7 @@ func main() {
|
||||
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
|
||||
}
|
||||
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
|
||||
mediumHealthProbe.Timeout = 10 * time.Second
|
||||
mediumHealthProbe.Timeout = 2 * time.Second
|
||||
}
|
||||
readinessProbes := []health.Probe{
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
|
||||
@@ -149,7 +152,7 @@ func main() {
|
||||
if gpuConfigured {
|
||||
gpuHeaders := map[string]string{"Authorization": "Bearer " + gpuKey}
|
||||
gpuProbe := health.OptionalHTTPProbeWithHeaders(readinessClient, strings.TrimRight(gpuURL, "/")+"/models", gpuHeaders)
|
||||
gpuProbe.Timeout = 10 * time.Second
|
||||
gpuProbe.Timeout = 2 * time.Second
|
||||
readinessProbes = append(readinessProbes, gpuProbe)
|
||||
}
|
||||
|
||||
@@ -168,6 +171,26 @@ func main() {
|
||||
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
|
||||
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
|
||||
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
|
||||
imageGenerator := multimodal.ImageGenerator(multimodal.ImageGenerationClient{BaseURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}})
|
||||
if strings.EqualFold(strings.TrimSpace(os.Getenv("LOCAL_IMAGE_PROVIDER_KIND")), "comfyui") {
|
||||
imageGenerator = multimodal.ComfyImageGenerationClient{BaseURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}, PollTimeout: 15 * time.Minute}
|
||||
}
|
||||
multimodalGateway := multimodalGateway{
|
||||
speech: multimodal.SpeechClient{BaseURL: envOr("LOCAL_SPEECH_PROVIDER_URL", "http://llm-local-speech:8080"), Client: &http.Client{Timeout: 120 * time.Second}},
|
||||
vision: multimodal.VisionClient{BaseURL: envOr("LOCAL_VISION_PROVIDER_URL", "http://llm-local-vision:8080/v1"), Client: &http.Client{Timeout: 180 * time.Second}},
|
||||
imageGenerator: imageGenerator,
|
||||
imageProviderURL: os.Getenv("LOCAL_IMAGE_PROVIDER_URL"),
|
||||
video: multimodal.VideoUnderstandingClient{BaseURL: os.Getenv("LOCAL_VIDEO_PROVIDER_URL"), Client: &http.Client{Timeout: 300 * time.Second}},
|
||||
model: envOr("LOCAL_VISION_MODEL", "/models/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"),
|
||||
imageModel: os.Getenv("LOCAL_IMAGE_MODEL"),
|
||||
videoModel: os.Getenv("LOCAL_VIDEO_MODEL"),
|
||||
now: time.Now,
|
||||
}
|
||||
mux.HandleFunc("POST /v1/multimodal/transcribe", multimodalGateway.handler(capability.MultimodalTranscribe))
|
||||
mux.HandleFunc("POST /v1/multimodal/understand", multimodalGateway.handler(capability.MultimodalUnderstand))
|
||||
mux.HandleFunc("POST /v1/multimodal/ocr", multimodalGateway.handler(capability.MultimodalRecognizeOCR))
|
||||
mux.HandleFunc("POST /v1/multimodal/video-understand", multimodalGateway.handler(capability.MultimodalUnderstandVideo))
|
||||
mux.HandleFunc("POST /v1/multimodal/generate-image", multimodalGateway.handler(capability.MultimodalGenerateImage))
|
||||
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)
|
||||
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/multimodal"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
const maxMultimodalUploadBytes = 12 << 20
|
||||
|
||||
type multimodalGateway struct {
|
||||
speech multimodal.SpeechClient
|
||||
vision multimodal.VisionClient
|
||||
imageGenerator multimodal.ImageGenerator
|
||||
imageProviderURL string
|
||||
video multimodal.VideoUnderstandingClient
|
||||
model string
|
||||
imageModel string
|
||||
videoModel string
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func (g multimodalGateway) handler(operation capability.MultimodalOperation) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxMultimodalUploadBytes)
|
||||
if err := r.ParseMultipartForm(maxMultimodalUploadBytes); err != nil {
|
||||
http.Error(w, "media upload is too large or invalid", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
if operation == capability.MultimodalGenerateImage {
|
||||
if strings.TrimSpace(g.imageProviderURL) == "" {
|
||||
http.Error(w, "local image generation capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
prompt := r.FormValue("prompt")
|
||||
image, err := g.imageGenerator.Generate(r.Context(), g.imageModel, prompt, imageRequestSize(prompt, r.FormValue("size")))
|
||||
if err != nil || strings.TrimSpace(image.DataBase64) == "" {
|
||||
logMultimodalFailure(operation, err)
|
||||
http.Error(w, "local image generation capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
result := capability.MultimodalResponse{Operation: operation, Provider: "local-image-generator", ObservedAt: g.now().UnixMilli(), ContentType: image.ContentType, DataBase64: image.DataBase64}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "media file is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(file, maxMultimodalUploadBytes+1))
|
||||
if err != nil || len(data) == 0 || len(data) > maxMultimodalUploadBytes {
|
||||
http.Error(w, "media file is invalid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
var text string
|
||||
if operation == capability.MultimodalTranscribe {
|
||||
text, err = g.speech.Transcribe(r.Context(), header.Filename, contentType, data)
|
||||
} else if operation == capability.MultimodalUnderstandVideo {
|
||||
if strings.TrimSpace(g.video.BaseURL) == "" {
|
||||
http.Error(w, "local video understanding capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
text, err = g.video.Describe(r.Context(), g.videoModel, header.Filename, contentType, data, r.FormValue("instruction"))
|
||||
} else {
|
||||
instruction := "Опиши, что изображено на фотографии, коротко и по существу. Не выдумывай нечитаемые детали."
|
||||
if operation == capability.MultimodalRecognizeOCR {
|
||||
instruction = "Точно распознай весь читаемый текст на изображении. Сохрани строки и символы; нечитаемые места обозначь как [неразборчиво]. Не добавляй пояснений."
|
||||
}
|
||||
text, err = g.vision.Describe(r.Context(), g.model, contentType, data, instruction)
|
||||
}
|
||||
if err != nil {
|
||||
logMultimodalFailure(operation, err)
|
||||
http.Error(w, "local media capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
result := capability.MultimodalResponse{Operation: operation, Text: strings.TrimSpace(text), ObservedAt: g.now().UnixMilli(), ConfidenceNote: "Результат получен локальным специализированным сервисом; нечитаемые или неоднозначные фрагменты требуют проверки."}
|
||||
if operation == capability.MultimodalTranscribe {
|
||||
result.Provider = "local-whisper"
|
||||
} else {
|
||||
result.Provider = "local-vision"
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
}
|
||||
|
||||
// imageRequestSize is a transport-level normalization for the local image
|
||||
// capability. An explicit, valid size always wins. When the caller only
|
||||
// expresses a conventional output-quality target, the capability translates it
|
||||
// to a supported provider size without exposing provider workflow details to
|
||||
// the interaction core. This policy is deliberately local and replaceable
|
||||
// with the image capability implementation.
|
||||
func imageRequestSize(prompt, requested string) string {
|
||||
if isImageSize(requested) {
|
||||
return strings.TrimSpace(requested)
|
||||
}
|
||||
words := strings.Fields(strings.ToLower(prompt))
|
||||
for index, word := range words {
|
||||
switch word {
|
||||
case "4k", "uhd":
|
||||
return "2048x2048"
|
||||
case "hd":
|
||||
return "1024x1024"
|
||||
case "ultra":
|
||||
if index+1 < len(words) && words[index+1] == "hd" {
|
||||
return "2048x2048"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isImageSize(value string) bool {
|
||||
var width, height int
|
||||
if _, err := fmt.Sscanf(strings.TrimSpace(value), "%dx%d", &width, &height); err != nil {
|
||||
return false
|
||||
}
|
||||
return width >= 256 && height >= 256 && width <= 2048 && height <= 2048
|
||||
}
|
||||
|
||||
func logMultimodalFailure(operation capability.MultimodalOperation, err error) {
|
||||
// Keep provider details in server diagnostics; never return them to chat.
|
||||
log.Printf("multimodal operation %s failed: %v", operation, err)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/multimodal"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestMultimodalTranscriptionHandlerReturnsLocalResult(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/inference" || r.Method != http.MethodPost {
|
||||
t.Fatalf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"text": "добрый день"})
|
||||
}))
|
||||
defer upstream.Close()
|
||||
server := multimodalGateway{speech: multimodal.SpeechClient{BaseURL: upstream.URL, Client: upstream.Client()}, now: time.Now}
|
||||
request := multipartRequest(t, "/v1/multimodal/transcribe", "voice.wav", []byte("audio"))
|
||||
response := httptest.NewRecorder()
|
||||
server.handler(capability.MultimodalTranscribe)(response, request)
|
||||
if response.Code != http.StatusOK || !bytes.Contains(response.Body.Bytes(), []byte("добрый день")) {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultimodalImageGenerationFailsClosedWhenSkillIsUnbound(t *testing.T) {
|
||||
server := multimodalGateway{now: time.Now}
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
if err := writer.WriteField("prompt", "green bird"); err != nil || writer.Close() != nil {
|
||||
t.Fatal("failed to build generation request")
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/multimodal/generate-image", body)
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
response := httptest.NewRecorder()
|
||||
server.handler(capability.MultimodalGenerateImage)(response, request)
|
||||
if response.Code != http.StatusBadGateway || !bytes.Contains(response.Body.Bytes(), []byte("unavailable")) {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageRequestSizeUsesExplicitSizeOrQualityTarget(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
prompt string
|
||||
requested string
|
||||
want string
|
||||
}{
|
||||
{name: "explicit", prompt: "нарисуй птицу", requested: "768x512", want: "768x512"},
|
||||
{name: "hd", prompt: "нарисуй птицу в HD", want: "1024x1024"},
|
||||
{name: "uhd", prompt: "draw a bird in UHD", want: "2048x2048"},
|
||||
{name: "ordinary", prompt: "нарисуй птицу", want: ""},
|
||||
{name: "invalid explicit", prompt: "нарисуй птицу", requested: "100x100", want: ""},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := imageRequestSize(test.prompt, test.requested); got != test.want {
|
||||
t.Fatalf("imageRequestSize(%q, %q) = %q, want %q", test.prompt, test.requested, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultimodalVideoUnderstandingFailsClosedWhenSkillIsUnbound(t *testing.T) {
|
||||
server := multimodalGateway{now: time.Now}
|
||||
request := multipartRequest(t, "/v1/multimodal/video-understand", "clip.mp4", []byte("video"))
|
||||
response := httptest.NewRecorder()
|
||||
server.handler(capability.MultimodalUnderstandVideo)(response, request)
|
||||
if response.Code != http.StatusBadGateway || !bytes.Contains(response.Body.Bytes(), []byte("unavailable")) {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func multipartRequest(t *testing.T, path, filename string, data []byte) *http.Request {
|
||||
t.Helper()
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := part.Write(data); err != nil || writer.Close() != nil {
|
||||
t.Fatal("failed to build multipart request")
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, path, body)
|
||||
request = request.WithContext(context.Background())
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return request
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
type chatRouteKind string
|
||||
|
||||
const (
|
||||
chatRouteResearch chatRouteKind = "RESEARCH"
|
||||
chatRouteConversation chatRouteKind = "CONVERSATION"
|
||||
chatRouteMedia chatRouteKind = "MEDIA"
|
||||
chatRouteAction chatRouteKind = "ACTION"
|
||||
chatRouteAgent chatRouteKind = "AGENT"
|
||||
)
|
||||
|
||||
type chatRoute struct {
|
||||
Kind chatRouteKind
|
||||
Capability capability.CapabilityKind
|
||||
Profile string
|
||||
Order int
|
||||
}
|
||||
|
||||
// buildChatRoutePlan creates one bounded route per meaning-bearing capability.
|
||||
// A failed research route must not suppress the local conversational route or
|
||||
// an unrelated action route.
|
||||
func buildChatRoutePlan(result interactiondomain.SemanticResult) []chatRoute {
|
||||
seen := make(map[capability.CapabilityKind]struct{})
|
||||
routes := make([]chatRoute, 0, len(result.CapabilityHints)+1)
|
||||
add := func(kind chatRouteKind, capabilityKind capability.CapabilityKind, profile string, order int) {
|
||||
if capabilityKind != "" {
|
||||
if _, exists := seen[capabilityKind]; exists {
|
||||
return
|
||||
}
|
||||
seen[capabilityKind] = struct{}{}
|
||||
}
|
||||
routes = append(routes, chatRoute{Kind: kind, Capability: capabilityKind, Profile: profile, Order: order})
|
||||
}
|
||||
for _, hint := range result.CapabilityHints {
|
||||
switch capability.CapabilityKind(hint) {
|
||||
case capability.CapabilityKindWebResearch:
|
||||
add(chatRouteResearch, capability.CapabilityKindWebResearch, "truth-research-v1", 10)
|
||||
case capability.CapabilityKindImageGeneration:
|
||||
add(chatRouteMedia, capability.CapabilityKindImageGeneration, "local-image-generation-v1", 5)
|
||||
case capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindVerifyFact:
|
||||
add(chatRouteConversation, capability.CapabilityKind(hint), "local-reasoning-v1", 20)
|
||||
case capability.CapabilityKindScheduleReminder:
|
||||
add(chatRouteAction, capability.CapabilityKindScheduleReminder, "continuity-reminder-v1", 30)
|
||||
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject:
|
||||
add(chatRouteAgent, capability.CapabilityKind(hint), "software-agent-v1", 40)
|
||||
case capability.CapabilityKindSendEmail, capability.CapabilityKindReadEmail:
|
||||
add(chatRouteAgent, capability.CapabilityKind(hint), "communication-agent-v1", 50)
|
||||
case capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
|
||||
add(chatRouteAgent, capability.CapabilityKind(hint), "diagnostics-agent-v1", 60)
|
||||
}
|
||||
}
|
||||
if result.NeedsCognition || len(routes) == 0 {
|
||||
add(chatRouteConversation, capability.CapabilityKindCognitiveReasoning, "local-reasoning-v1", 20)
|
||||
}
|
||||
sort.SliceStable(routes, func(i, j int) bool { return routes[i].Order < routes[j].Order })
|
||||
return routes
|
||||
}
|
||||
|
||||
func hasChatRoute(routes []chatRoute, kind chatRouteKind) bool {
|
||||
for _, route := range routes {
|
||||
if route.Kind == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// specializedRouteBypassesGeneralInterpretation keeps a concrete capability
|
||||
// route stable. General cognition may still be used later to assess evidence,
|
||||
// but it must not replace an executable research or media route with a vague
|
||||
// conversational clarification before the capability is attempted.
|
||||
func specializedRouteBypassesGeneralInterpretation(routes []chatRoute) bool {
|
||||
return hasChatRoute(routes, chatRouteMedia) || hasChatRoute(routes, chatRouteResearch)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestBuildChatRoutePlanPreservesMultipleMeaningfulRoutes(t *testing.T) {
|
||||
planned := buildChatRoutePlan(interactiondomain.SemanticResult{
|
||||
NeedsCognition: true,
|
||||
CapabilityHints: []string{"WEB_RESEARCH", "MODIFY_CODE", "RUN_TESTS", "SCHEDULE_REMINDER"},
|
||||
})
|
||||
if len(planned) != 5 {
|
||||
t.Fatalf("expected five deduplicated routes, got %+v", planned)
|
||||
}
|
||||
want := []struct {
|
||||
kind chatRouteKind
|
||||
profile string
|
||||
cap capability.CapabilityKind
|
||||
}{
|
||||
{chatRouteResearch, "truth-research-v1", capability.CapabilityKindWebResearch},
|
||||
{chatRouteConversation, "local-reasoning-v1", capability.CapabilityKindCognitiveReasoning},
|
||||
{chatRouteAction, "continuity-reminder-v1", capability.CapabilityKindScheduleReminder},
|
||||
{chatRouteAgent, "software-agent-v1", capability.CapabilityKindModifyCode},
|
||||
{chatRouteAgent, "software-agent-v1", capability.CapabilityKindRunTests},
|
||||
}
|
||||
for index, expected := range want {
|
||||
if planned[index].Kind != expected.kind || planned[index].Profile != expected.profile || planned[index].Capability != expected.cap {
|
||||
t.Fatalf("route %d = %+v, want %+v", index, planned[index], expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildChatRoutePlanAlwaysKeepsLocalConversationRoute(t *testing.T) {
|
||||
planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: true})
|
||||
if len(planned) != 1 || planned[0].Kind != chatRouteConversation || planned[0].Profile != "local-reasoning-v1" {
|
||||
t.Fatalf("unexpected local route: %+v", planned)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecializedRouteBypassesGeneralInterpretation(t *testing.T) {
|
||||
if !specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteResearch}}) {
|
||||
t.Fatal("research route must execute before general cognition")
|
||||
}
|
||||
if !specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteMedia}}) {
|
||||
t.Fatal("media route must execute before general cognition")
|
||||
}
|
||||
if specializedRouteBypassesGeneralInterpretation([]chatRoute{{Kind: chatRouteConversation}}) {
|
||||
t.Fatal("conversation-only route must retain general cognition")
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -20,6 +25,109 @@ import (
|
||||
type cognitiveInterpretationClient struct {
|
||||
baseURL *url.URL
|
||||
client *http.Client
|
||||
media *http.Client
|
||||
}
|
||||
|
||||
// ReviewResponse asks the cognitive gateway to turn a locally assembled draft
|
||||
// into a natural answer. The draft remains the safe fallback when cognition is
|
||||
// unavailable; the model does not mutate canonical state or execute actions.
|
||||
func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, session identity.CreatorSession, userMessage, draft, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
|
||||
requestID, err := ids.New()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты редактор финального ответа Домового. Проверь черновик по смыслу и подготовь естественный ответ человеку. Учитывай весь запрос, включая недостающие параметры для конкретного результата. Если данных не хватает, задай один короткий полезный уточняющий вопрос, например откуда и куда ехать. Не выдавай поисковые сниппеты как выполненное действие и не показывай служебные URL поисковой выдачи. Сохрани проверяемые факты, названия источников, прямые ссылки и ограничения. Не выдумывай факты и не утверждай невыполненные действия. Сначала внутренне проверь, что ответ начинается с содержания, решающего вопрос. Не повторяй вопрос пользователя в качестве заголовка или вступления, если это не нужно для смысла. Представление, имя помощника, приветствие или предложение дополнительной помощи допустимы только если пользователь сам спросил об этом; иначе убери их. Верни только готовый ответ без разбора проверки, служебных слов и упоминания моделей. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
}
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/advisories"
|
||||
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
|
||||
ActorID: session.ActorID, PrincipalID: session.PrincipalID,
|
||||
ScopeID: authority.ScopeID("domovoy-global-scope"), Message: prompt,
|
||||
Context: context, Locale: "ru", MaxOutputBytes: 2048, DataPolicy: capability.DataPolicyLocalOnly,
|
||||
Meaning: cognitive.TaskMeaning{
|
||||
SpeechAct: "RESPONSE_REVIEW",
|
||||
CapabilityHints: meaning.CapabilityHints,
|
||||
RequestedOutcome: meaning.RequestedOutcome,
|
||||
InformationRequestKind: meaning.InformationRequestKind,
|
||||
ConfidenceMillis: meaning.ConfidenceMillis,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("cognitive response review status %d", response.StatusCode)
|
||||
}
|
||||
var result cognitive.GatewayAdvisoryResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
|
||||
return "", fmt.Errorf("cognitive response review incomplete: %s", result.FinishReason)
|
||||
}
|
||||
return strings.TrimSpace(result.AdvisoryText), nil
|
||||
}
|
||||
|
||||
// AnswerStableKnowledge asks the local cognitive route for a short answer to a
|
||||
// non-current question only after evidence retrieval did not produce a usable
|
||||
// result. The model decides whether its knowledge is stable enough; it must
|
||||
// explicitly decline changing or source-dependent facts instead of guessing.
|
||||
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
|
||||
requestID, err := ids.New()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
}
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/advisories"
|
||||
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
|
||||
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
|
||||
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
|
||||
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: 1024, DataPolicy: capability.DataPolicyLocalOnly,
|
||||
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
|
||||
}
|
||||
var result cognitive.GatewayAdvisoryResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
|
||||
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
|
||||
}
|
||||
return strings.TrimSpace(result.AdvisoryText), nil
|
||||
}
|
||||
|
||||
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
|
||||
@@ -30,7 +138,7 @@ func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cogni
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return cognitiveInterpretationClient{baseURL: base, client: client}, nil
|
||||
return cognitiveInterpretationClient{baseURL: base, client: client, media: &http.Client{Timeout: 15 * time.Minute}}, nil
|
||||
}
|
||||
|
||||
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
|
||||
@@ -60,13 +168,107 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c cognitiveInterpretationClient) GenerateImage(ctx context.Context, prompt, size string) (capability.MultimodalResponse, error) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
if err := writer.WriteField("prompt", prompt); err != nil {
|
||||
return capability.MultimodalResponse{}, err
|
||||
}
|
||||
if strings.TrimSpace(size) != "" {
|
||||
if err := writer.WriteField("size", size); err != nil {
|
||||
return capability.MultimodalResponse{}, err
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return capability.MultimodalResponse{}, err
|
||||
}
|
||||
target := *c.baseURL
|
||||
target.Path = "/v1/multimodal/generate-image"
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), &body)
|
||||
if err != nil {
|
||||
return capability.MultimodalResponse{}, err
|
||||
}
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
client := c.media
|
||||
if client == nil {
|
||||
client = c.client
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return capability.MultimodalResponse{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return capability.MultimodalResponse{}, fmt.Errorf("image generation status %d", response.StatusCode)
|
||||
}
|
||||
var result capability.MultimodalResponse
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
|
||||
return capability.MultimodalResponse{}, err
|
||||
}
|
||||
if strings.TrimSpace(result.DataBase64) == "" {
|
||||
return capability.MultimodalResponse{}, fmt.Errorf("image generation returned empty payload")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
|
||||
switch result.Interpretation.Applicability {
|
||||
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
|
||||
return true
|
||||
default:
|
||||
return shouldRequestSemanticInterpretation(result)
|
||||
}
|
||||
|
||||
// shouldRequestSemanticInterpretation keeps the deterministic layer as a
|
||||
// fast reflex, not as the meaning authority. Questions, requests, directives,
|
||||
// mixed signals and capability-bearing messages go through cognitive
|
||||
// interpretation; only stable low-risk conversational/assertive reflexes may
|
||||
// answer without it.
|
||||
func shouldRequestSemanticInterpretation(result interactiondomain.SemanticResult) bool {
|
||||
if result.Interpretation.Clarification == interaction.ClarificationRequired {
|
||||
return false
|
||||
}
|
||||
// Structured, read-only information intents already have a semantic owner
|
||||
// in core-runtime. Sending them through the slower advisory interpretation
|
||||
// path only adds latency and can make a healthy runtime look unavailable.
|
||||
if result.InformationRequestKind != "" {
|
||||
return false
|
||||
}
|
||||
if result.NeedsCognition || len(result.CapabilityHints) > 0 {
|
||||
return true
|
||||
}
|
||||
if result.Interpretation.Applicability != interaction.SemanticApplicable {
|
||||
return true
|
||||
}
|
||||
switch result.Interpretation.SpeechAct {
|
||||
case interaction.SpeechActConversational, interaction.SpeechActConfirmation, interaction.SpeechActRejection:
|
||||
return false
|
||||
case interaction.SpeechActPreference:
|
||||
// Explicit user preferences are durable interaction context. A sampled
|
||||
// advisory audit must not reinterpret or erase them.
|
||||
return false
|
||||
case interaction.SpeechActAssertion:
|
||||
return result.Interpretation.ConfidenceMillis < 850
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
deterministicReflexAuditInterval = 10 * time.Minute
|
||||
deterministicReflexAuditSample = uint32(16)
|
||||
)
|
||||
|
||||
// shouldAuditDeterministicReflex samples stable reflexes in a time bucket.
|
||||
// This is observability, not a correctness gate: a failed audit leaves the
|
||||
// reflex usable, while a successful disagreement can replace it with the
|
||||
// cognitive candidate and becomes durable evidence through the normal
|
||||
// candidate store.
|
||||
func shouldAuditDeterministicReflex(result interactiondomain.SemanticResult, utteranceID string, now time.Time) bool {
|
||||
if shouldRequestSemanticInterpretation(result) || utteranceID == "" {
|
||||
return false
|
||||
}
|
||||
bucket := now.Unix() / int64(deterministicReflexAuditInterval/time.Second)
|
||||
hash := fnv.New32a()
|
||||
_, _ = hash.Write([]byte(utteranceID))
|
||||
_, _ = hash.Write([]byte(fmt.Sprintf(":%d", bucket)))
|
||||
return hash.Sum32()%deterministicReflexAuditSample == 0
|
||||
}
|
||||
|
||||
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
|
||||
|
||||
@@ -1,12 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var request cognitive.GatewayAdvisoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode review request: %v", err)
|
||||
}
|
||||
if request.Message == "" || request.Meaning.SpeechAct == "" {
|
||||
t.Fatalf("review request did not carry semantic context: %+v", request)
|
||||
}
|
||||
if !strings.Contains(request.Message, "ответ начинается с содержания") || !strings.Contains(request.Message, "Не повторяй вопрос пользователя") || !strings.Contains(request.Message, "только если пользователь сам спросил") {
|
||||
t.Fatalf("review request lacks the conversational quality contract: %q", request.Message)
|
||||
}
|
||||
if len(request.Context) != 1 || !strings.Contains(request.Context[0].Text, "красный цвет") {
|
||||
t.Fatalf("review request did not carry compact personal memory: %+v", request.Context)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Меня зовут Дед. Я Домовой.", FinishReason: cognitive.FinishCompleted})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reviewed, err := client.ReviewResponse(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "кто ты?", "Я — Домовой.", "Предпочтение пользователя (его сообщение): красный цвет", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reviewed != "Меня зовут Дед. Я Домовой." {
|
||||
t.Fatalf("unexpected reviewed response %q", reviewed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var request cognitive.GatewayAdvisoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatalf("decode stable knowledge request: %v", err)
|
||||
}
|
||||
if request.DataPolicy != capability.DataPolicyLocalOnly || request.Meaning.SpeechAct != "STABLE_KNOWLEDGE_ANSWER" {
|
||||
t.Fatalf("unexpected stable knowledge request: %+v", request)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Санкт-Петербург — город в России.", FinishReason: cognitive.FinishCompleted})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
answer, err := client.AnswerStableKnowledge(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Питер — это где?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
|
||||
if err != nil || answer != "Санкт-Петербург — город в России." {
|
||||
t.Fatalf("answer=%q err=%v", answer, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
|
||||
t.Fatalf("unexpected image route: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
t.Fatalf("parse image request: %v", err)
|
||||
}
|
||||
if !strings.Contains(r.FormValue("prompt"), "собаку") {
|
||||
t.Fatalf("prompt was not preserved: %q", r.FormValue("prompt"))
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalGenerateImage, ContentType: "image/png", DataBase64: "cG5n"})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
image, err := client.GenerateImage(context.Background(), "нарисуй собаку", "")
|
||||
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
|
||||
t.Fatalf("image=%+v err=%v", image, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewPreservesResearchEvidence(t *testing.T) {
|
||||
draft := "Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)"
|
||||
if reviewPreservesResearchEvidence("Нашёл вариант на Авиасейлс.", draft) {
|
||||
t.Fatal("review without the direct source must be rejected")
|
||||
}
|
||||
if !reviewPreservesResearchEvidence("Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)", draft) {
|
||||
t.Fatal("review with the direct source must be accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleQuestionUsesStrictInterpretationForSemanticRouting(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
@@ -34,3 +131,51 @@ func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
|
||||
t.Fatal("operational question must retain strict interpretation routing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructuredInventoryUsesDirectReadOnlySemanticRoute(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{
|
||||
Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActDirective,
|
||||
Applicability: interaction.SemanticApplicable,
|
||||
ConfidenceMillis: 980,
|
||||
},
|
||||
InformationRequestKind: interaction.InformationRequestCognitiveModelInventory,
|
||||
}
|
||||
if shouldRequestSemanticInterpretation(result) {
|
||||
t.Fatal("structured model inventory must use the direct read-only semantic route")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStableReflexIsPeriodicallySampledForDrift(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActConversational, Applicability: interaction.SemanticApplicable,
|
||||
}}
|
||||
found := false
|
||||
for minute := 0; minute < 160; minute++ {
|
||||
if shouldAuditDeterministicReflex(result, "utterance-drift-probe", time.Unix(int64(minute)*60, 0)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("stable reflex was never sampled during the audit window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeaningfulSignalsAreNotReflexOnly(t *testing.T) {
|
||||
for _, act := range []interaction.SpeechAct{interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective} {
|
||||
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: act, Applicability: interaction.SemanticApplicable}}
|
||||
if !shouldRequestSemanticInterpretation(result) {
|
||||
t.Fatalf("%s must prefer cognitive interpretation", act)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitPreferenceIsNotReinterpretedByCognitiveAudit(t *testing.T) {
|
||||
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
|
||||
SpeechAct: interaction.SpeechActPreference, Applicability: interaction.SemanticApplicable,
|
||||
}}
|
||||
if shouldRequestSemanticInterpretation(result) || shouldAuditDeterministicReflex(result, "preference", time.Unix(0, 0)) {
|
||||
t.Fatal("explicit preference must remain on the deterministic durable-context path")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestHouseholdResearchAndActionRoutesRemainIndependent(t *testing.T) {
|
||||
candidate := interactiondomain.BuildDeterministicCandidate("проверь погоду в Казани, найди кафе рядом и напомни вечером позвонить", "", time.Unix(10, 0).UnixMilli())
|
||||
planned := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
|
||||
want := map[chatRouteKind]bool{chatRouteResearch: false, chatRouteConversation: false, chatRouteAction: false}
|
||||
for _, route := range planned {
|
||||
if _, ok := want[route.Kind]; ok {
|
||||
want[route.Kind] = true
|
||||
}
|
||||
}
|
||||
for kind, present := range want {
|
||||
if !present {
|
||||
t.Fatalf("route %s missing for mixed household request: candidate=%+v routes=%+v", kind, candidate, planned)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHouseholdCreativeRouteDoesNotUseResearch(t *testing.T) {
|
||||
candidate := interactiondomain.BuildDeterministicCandidate("расскажи сказку о Колобке", "", time.Unix(10, 0).UnixMilli())
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch {
|
||||
t.Fatalf("creative request selected web research: %+v", candidate)
|
||||
}
|
||||
}
|
||||
if !interactiondomain.IsCreativeRequest("расскажи сказку о Колобке") {
|
||||
t.Fatal("creative request was not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHouseholdMarketplaceBrandSearchUsesResearchRoute(t *testing.T) {
|
||||
candidate := interactiondomain.BuildDeterministicCandidate("найди на WB бренд marka-wear", "", time.Unix(10, 0).UnixMilli())
|
||||
decomposition := interactiondomain.DecomposeTaskWithLexicon("найди на WB бренд marka-wear", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}}, SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}}})
|
||||
if decomposition.Intent != interactiondomain.TaskIntentSearch || decomposition.Object != "BRAND" || decomposition.Source != "WILDBERRIES" || decomposition.Target != "marka-wear" || len(decomposition.Missing) != 0 {
|
||||
t.Fatalf("marketplace request was not split into executable atoms: %+v", decomposition)
|
||||
}
|
||||
routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
|
||||
if !hasChatRoute(routes, chatRouteResearch) {
|
||||
t.Fatalf("marketplace brand request did not select research route: candidate=%+v routes=%+v", candidate, routes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHouseholdSemanticMemoryControlsMarketplaceAlias(t *testing.T) {
|
||||
withoutMemory := interactiondomain.DecomposeTask("найди на WB бренд marka-wear")
|
||||
if withoutMemory.Source != "" || withoutMemory.Object != "" {
|
||||
t.Fatalf("core invented marketplace meaning without memory: %+v", withoutMemory)
|
||||
}
|
||||
withMemory := interactiondomain.DecomposeTaskWithLexicon("поищи марку marka-wear в Wildberries", interactiondomain.TaskLexicon{
|
||||
ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "марку", Value: "BRAND"}},
|
||||
SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wildberries", Value: "WILDBERRIES"}},
|
||||
})
|
||||
if withMemory.Intent != interactiondomain.TaskIntentSearch || withMemory.Object != "BRAND" || withMemory.Source != "WILDBERRIES" || withMemory.Target != "marka-wear" || !withMemory.NeedsResearch {
|
||||
t.Fatalf("semantic memory did not control marketplace decomposition: %+v", withMemory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHouseholdBearDrawingUsesCreativeLocalRoute(t *testing.T) {
|
||||
candidate := interactiondomain.BuildDeterministicCandidate("нарисуй бурого медведя", "", time.Unix(10, 0).UnixMilli())
|
||||
decomposition := interactiondomain.DecomposeTaskWithLexicon("нарисуй бурого медведя", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}})
|
||||
if decomposition.Intent != interactiondomain.TaskIntentCreate || decomposition.Object != "IMAGE" || decomposition.Target != "бурого медведя" || decomposition.NeedsResearch {
|
||||
t.Fatalf("creative request was not split into local execution atoms: %+v", decomposition)
|
||||
}
|
||||
routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints})
|
||||
if !hasChatRoute(routes, chatRouteMedia) {
|
||||
t.Fatalf("creative image request did not select media route: candidate=%+v routes=%+v", candidate, routes)
|
||||
}
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch {
|
||||
t.Fatalf("bear drawing selected web research: %+v", candidate)
|
||||
}
|
||||
}
|
||||
for _, message := range []string{"нарисуй бурого медведя", "нарисуй птицу", "нарисуй зеленую птицу", "нарисуй птицу в HD", "нарисуй коня"} {
|
||||
if got := interactiondomain.CreativeDeterministicReply(message); got != "" {
|
||||
t.Fatalf("creative request %q received a canned reply=%q", message, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHouseholdRouteMatrixKeepsMeaningBoundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
message string
|
||||
wantResearch bool
|
||||
wantCreative bool
|
||||
wantHint string
|
||||
}{
|
||||
{message: "найди на WB бренд marka-wear", wantResearch: true},
|
||||
{message: "какая погода в Казани?", wantResearch: true},
|
||||
{message: "найди билеты до Сочи", wantResearch: true},
|
||||
{message: "нарисуй птицу", wantCreative: true},
|
||||
{message: "расскажи сказку о Колобке", wantCreative: true},
|
||||
{message: "что такое гравитация?", wantHint: "COGNITIVE_REASONING"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
candidate := interactiondomain.BuildDeterministicCandidate(test.message, "", time.Unix(10, 0).UnixMilli())
|
||||
if interactiondomain.IsCreativeRequest(test.message) != test.wantCreative {
|
||||
t.Fatalf("message=%q creative=%v want=%v candidate=%+v", test.message, interactiondomain.IsCreativeRequest(test.message), test.wantCreative, candidate)
|
||||
}
|
||||
gotResearch := false
|
||||
gotHint := false
|
||||
for _, hint := range candidate.CapabilityHints {
|
||||
if hint == "WEB_RESEARCH" {
|
||||
gotResearch = true
|
||||
}
|
||||
if hint == test.wantHint {
|
||||
gotHint = true
|
||||
}
|
||||
}
|
||||
if gotResearch != test.wantResearch {
|
||||
t.Fatalf("message=%q research=%v want=%v candidate=%+v", test.message, gotResearch, test.wantResearch, candidate)
|
||||
}
|
||||
if test.wantHint != "" && !gotHint {
|
||||
t.Fatalf("message=%q missing hint=%q candidate=%+v", test.message, test.wantHint, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
func TestSpecializedInformationReplyIsNotOverwrittenByResearch(t *testing.T) {
|
||||
for _, kind := range []interaction.InformationRequestKind{
|
||||
interaction.InformationRequestMemoryInventory,
|
||||
interaction.InformationRequestCapabilityInventory,
|
||||
interaction.InformationRequestCognitiveModelInventory,
|
||||
interaction.InformationRequestCognitiveChannelStatus,
|
||||
interaction.InformationRequestCognitiveRecovery,
|
||||
} {
|
||||
if shouldRunGeneralResearch(kind) {
|
||||
t.Fatalf("specialized information request %q must bypass general research", kind)
|
||||
}
|
||||
}
|
||||
if !shouldRunGeneralResearch(interaction.InformationRequestTimeDifference) {
|
||||
t.Fatal("time difference must use the current-data research route")
|
||||
}
|
||||
if !shouldRunGeneralResearch("") {
|
||||
t.Fatal("ordinary research requests must still use general research")
|
||||
}
|
||||
}
|
||||
+809
-57
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,183 @@ import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
)
|
||||
|
||||
func TestHumanResearchExcerptRemovesSearchResultHeading(t *testing.T) {
|
||||
got := humanResearchExcerpt("погода в Санкт-Петербурге", "Погода в Санкт-Петербурге — Прогноз погоды — Сейчас пасмурно, температура +14°")
|
||||
if got != "Сейчас пасмурно, температура +14°" || strings.Contains(got, "Прогноз погоды") {
|
||||
t.Fatalf("human research excerpt = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightEvidenceDoesNotClaimMissingDepartureTime(t *testing.T) {
|
||||
if !nearestFlightQuery("ближайший рейс Москва Казань сегодня") {
|
||||
t.Fatal("nearest flight query was not recognized")
|
||||
}
|
||||
if containsFlightTimeEvidence("Казань составляет 1 ч 25 мин. График вылетов доступен онлайн") {
|
||||
t.Fatal("duration was mistaken for a departure time")
|
||||
}
|
||||
if !containsFlightTimeEvidence("Ближайший вылет в 18:40") {
|
||||
t.Fatal("departure time was not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://naidy.example", Title: "Мебельная фурнитура"}, Excerpt: epistemics.SourceExcerpt{Text: "Комплектующие для мебели оптом и в розницу."}},
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://rasp.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание самолётов Москва — Казань, время вылета и прилёта."}},
|
||||
}}
|
||||
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
|
||||
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Мебельная") {
|
||||
t.Fatalf("unrelated source leaked into flight reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
|
||||
}}
|
||||
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
|
||||
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
|
||||
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
|
||||
query := "ближайшие авиарейсы Москва Казань сегодня"
|
||||
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
|
||||
if !researchFindingMatchesIntent(query, matched) {
|
||||
t.Fatal("route evidence with both endpoints was rejected")
|
||||
}
|
||||
unrelated := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань", Locator: "https://example.test/moscow-kazan"}, Excerpt: epistemics.SourceExcerpt{Text: "Рейсы Москва — Казань — Ближайшие к Казани города; цена указана ориентировочно."}}
|
||||
if researchFindingMatchesIntent(query, unrelated) {
|
||||
t.Fatal("one-ended travel evidence was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTicketReplyRejectsGenericCategoryPageWithoutDestination(t *testing.T) {
|
||||
query := "найди билеты до Ялты сегодня"
|
||||
generic := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Купить билеты", Locator: "https://tickets.example"}, Excerpt: epistemics.SourceExcerpt{Text: "Поиск авиабилетов среди множества направлений."}}
|
||||
if researchFindingMatchesIntent(query, generic) {
|
||||
t.Fatal("generic ticket category was presented as destination result")
|
||||
}
|
||||
relevant := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Билеты"}, Excerpt: epistemics.SourceExcerpt{Text: "Билеты до Ялты: варианты на сегодня."}}
|
||||
if !researchFindingMatchesIntent(query, relevant) {
|
||||
t.Fatal("destination-specific ticket result was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistanceReplyRejectsCalculatorPageWithoutDistance(t *testing.T) {
|
||||
query := "расстояние между Москве и Сочи в километрах по прямой и по дороге"
|
||||
calculator := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Расчет расстояний"}, Excerpt: epistemics.SourceExcerpt{Text: "Калькулятор рассчитывает точное расстояние между городами."}}
|
||||
if researchFindingMatchesIntent(query, calculator) {
|
||||
t.Fatal("distance calculator landing page was presented without a measured distance")
|
||||
}
|
||||
measured := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Маршрут"}, Excerpt: epistemics.SourceExcerpt{Text: "Расстояние по дороге составляет 1 620 км."}}
|
||||
if !researchFindingMatchesIntent(query, measured) {
|
||||
t.Fatal("measured distance evidence was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
||||
State: epistemics.FindingSupported,
|
||||
Source: epistemics.Source{Locator: "https://market.example", Title: "Интернет-магазин"},
|
||||
Excerpt: epistemics.SourceExcerpt{Text: "Широкий ассортимент товаров и услуг."},
|
||||
}}}
|
||||
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
|
||||
if backed || !strings.Contains(reply, "marka-wear") || strings.Contains(reply, "wildberries") || !strings.Contains(reply, "уточните написание") {
|
||||
t.Fatalf("generic page was presented as a brand result: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
||||
State: epistemics.FindingSupported,
|
||||
Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"},
|
||||
Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."},
|
||||
}}}
|
||||
reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil)
|
||||
if backed || !strings.Contains(reply, "Проверяемых данных") {
|
||||
t.Fatalf("dictionary finding was presented as local place result: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
||||
State: epistemics.FindingSupported,
|
||||
Source: epistemics.Source{Locator: "https://market.example/marka-wear", Title: "Marka-wear"},
|
||||
Excerpt: epistemics.SourceExcerpt{Text: "Бренд marka-wear: товары и карточки."},
|
||||
}}}
|
||||
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
|
||||
if !backed || !strings.Contains(strings.ToLower(reply), "marka-wear") {
|
||||
t.Fatalf("concrete identifier finding was rejected: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchIdentifiersExcludeSearchDate(t *testing.T) {
|
||||
identifiers := requestedResearchIdentifiers("ближайший поезд в Петербург сегодня 26.08.2026")
|
||||
if len(identifiers) != 0 {
|
||||
t.Fatalf("search date was treated as a concrete identifier: %#v", identifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchQueryKeepsDistanceRelationBeforeSearchNormalization(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||||
query := interactiondomain.TravelResearchQuery("сколько километров до Сочи?", history, time.Time{})
|
||||
query = interactiondomain.SearchQueryWithLexicon(query, interactiondomain.TaskLexicon{})
|
||||
if query != "расстояние между Москве и Сочи в километрах по прямой и по дороге" {
|
||||
t.Fatalf("distance query lost remembered origin: %q", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
|
||||
if reviewMayReplacePlanningReply(false, false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") {
|
||||
t.Fatal("review replaced an honest unbacked research outcome")
|
||||
}
|
||||
if !reviewMayReplacePlanningReply(false, false, true, true, "Расписание. Источник: https://example.test", "Расписание. Источник: https://example.test") {
|
||||
t.Fatal("review should be able to improve a source-backed research outcome")
|
||||
}
|
||||
if reviewMayReplacePlanningReply(false, false, true, true, "Расписание без ссылки", "Расписание. Источник: https://example.test") {
|
||||
t.Fatal("review dropped the research evidence")
|
||||
}
|
||||
if !reviewMayReplacePlanningReply(false, false, false, false, "Нормальный ответ", "Черновик") {
|
||||
t.Fatal("ordinary conversation review was unexpectedly blocked")
|
||||
}
|
||||
if !reviewMayReplacePlanningReply(true, true, false, false, "Санкт-Петербург — город в России.", "Меня зовут Домовой. Санкт-Петербург — город в России.") {
|
||||
t.Fatal("stable-knowledge draft must be eligible for semantic quality review")
|
||||
}
|
||||
// The caller must not mark a source-backed research result as immutable
|
||||
// semantic state: it may be rewritten, but only with its evidence retained.
|
||||
if !reviewMayReplacePlanningReply(false, false, true, true, "Из Москвы в Сочи можно добраться самолётом или поездом. Источник: https://example.test", "Сырые результаты. Источник: https://example.test") {
|
||||
t.Fatal("source-backed research draft must be eligible for human-facing review")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
|
||||
semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}}
|
||||
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
|
||||
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
|
||||
}
|
||||
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
|
||||
t.Fatal("reply-only cognition path was incorrectly rejected")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
|
||||
t.Fatal("direct capability was incorrectly replaced by a textual reply")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
|
||||
t.Fatal("media route was incorrectly replaced by a textual reply")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) {
|
||||
t.Fatal("durable work was incorrectly replaced by a textual reply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
request := httptest.NewRequest("GET", "/", nil)
|
||||
request.Header.Set("Authorization", "Basic value")
|
||||
@@ -27,6 +200,59 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestAssistantNameUsesNewestDurablePreference(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{Content: "я назову тебя Дед", ReceivedAtUnixMillis: 20},
|
||||
{Content: "теперь тебя зовут Фея", ReceivedAtUnixMillis: 30},
|
||||
}
|
||||
if got := latestAssistantName(history); got != "Фея" {
|
||||
t.Fatalf("latest assistant name = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanMemoryInventoryUsesNewestPreference(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{Content: "мне нравится синий цвет", ReceivedAtUnixMillis: 20},
|
||||
{Content: "мне нравится красный цвет", ReceivedAtUnixMillis: 30},
|
||||
}
|
||||
got := humanMemoryInventory(history, "Фея")
|
||||
if !strings.Contains(got, "красный") || strings.Contains(got, "синий") {
|
||||
t.Fatalf("memory inventory did not select newest preference: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsScheduleTimeEvidence(t *testing.T) {
|
||||
if containsScheduleTimeEvidence("расписание поездов и цена билета") {
|
||||
t.Fatal("a price or schedule label is not a departure time")
|
||||
}
|
||||
if !containsScheduleTimeEvidence("Отправление в 07:45") {
|
||||
t.Fatal("expected an explicit departure time to be recognized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternetStatusQueryRecognizesConnectionChecks(t *testing.T) {
|
||||
for _, query := range []string{"проверь, работает ли интернет", "есть доступ к сети?"} {
|
||||
if !isInternetStatusQuery(query) {
|
||||
t.Fatalf("connection check was not recognized: %q", query)
|
||||
}
|
||||
}
|
||||
if isInternetStatusQuery("найди интернет-магазин") {
|
||||
t.Fatal("ordinary search must not be treated as a connectivity check")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternetStatusReplyDoesNotLeakIncidentalSearchResults(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
||||
State: epistemics.FindingSupported,
|
||||
Source: epistemics.Source{Locator: "https://example.test", Title: "Incidental page"},
|
||||
Excerpt: epistemics.SourceExcerpt{Text: "A response was received."},
|
||||
}}}
|
||||
reply, backed := renderResearchReply("проверь, работает ли интернет", result, nil)
|
||||
if !backed || reply != "Интернет работает: Домовой получил ответ от внешнего источника." {
|
||||
t.Fatalf("unexpected internet status reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
|
||||
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
|
||||
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
|
||||
@@ -34,16 +260,23 @@ func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentModelReplyDoesNotInventProvider(t *testing.T) {
|
||||
reply := humanCurrentCognitiveModelReply(capability.Health{State: capability.HealthDegraded, CapabilityID: "provider-secret-model"}, nil)
|
||||
if !strings.Contains(reply, "встроенным режимом") || !strings.Contains(reply, "ограничениями") || strings.Contains(reply, "provider-secret-model") {
|
||||
t.Fatalf("unexpected current-model reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
|
||||
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
|
||||
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
|
||||
if reply != "Канал мышления недоступен. Домовой остаётся на связи." {
|
||||
t.Fatalf("unexpected configuration reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
|
||||
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
|
||||
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
|
||||
if strings.Contains(reply, "когнитивный канал") || strings.Contains(reply, "Повторю") || !strings.Contains(reply, "не выдумываю") {
|
||||
t.Fatalf("unexpected degraded reply: %q", reply)
|
||||
}
|
||||
}
|
||||
@@ -74,11 +307,21 @@ func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
|
||||
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
|
||||
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
|
||||
}}, nil)
|
||||
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
|
||||
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "активна") {
|
||||
t.Fatalf("unsafe or incomplete inventory: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanCognitiveModelInventoryDeduplicatesAndPrioritizesAvailable(t *testing.T) {
|
||||
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{
|
||||
{Binding: capability.CognitiveBinding{Activation: capability.ActivationInactive}, Provider: capability.ProviderInstance{Health: capability.HealthUnavailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthUnavailable}},
|
||||
{Binding: capability.CognitiveBinding{Activation: capability.ActivationActive}, Provider: capability.ProviderInstance{Health: capability.HealthAvailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthAvailable}},
|
||||
}, nil)
|
||||
if strings.Count(text, "same-model") != 1 || !strings.Contains(text, "same-model — активна") {
|
||||
t.Fatalf("expected one active model entry, got %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
|
||||
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
|
||||
|
||||
@@ -40,6 +40,11 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
|
||||
if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") {
|
||||
t.Fatalf("/chat does not render build metadata: %q", body)
|
||||
}
|
||||
for _, required := range []string{"media-tools", "audio-input", "image-input", "ocr-input"} {
|
||||
if !strings.Contains(body, required) {
|
||||
t.Fatalf("/chat is missing multimodal control %q", required)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"/", "/chat.html"} {
|
||||
alternate := httptest.NewRecorder()
|
||||
handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
@@ -60,6 +65,11 @@ func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
|
||||
t.Fatalf("chat script is missing recovery message %q", required)
|
||||
}
|
||||
}
|
||||
for _, required := range []string{"uploadMedia", "/api/multimodal/transcribe", "/api/multimodal/understand", "/api/multimodal/ocr"} {
|
||||
if !strings.Contains(script, required) {
|
||||
t.Fatalf("chat script is missing multimodal route %q", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
|
||||
@@ -158,14 +168,21 @@ func TestChatMessageDoesNotExposeUnverifiedQuestionAsFact(t *testing.T) {
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "небезопасная модельная догадка"})
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindWebResearch}, FallbackText: "небезопасная модельная догадка"})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unverified question must not reach advisory: %s %s", r.Method, r.URL.Path)
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/advisories" {
|
||||
t.Fatalf("unexpected cognitive route: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var request cognitive.GatewayAdvisoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Я продолжаю искать проверяемый ответ через доступный источник.", FinishReason: cognitive.FinishCompleted})
|
||||
}))
|
||||
defer cognitiveRuntime.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
|
||||
@@ -184,8 +201,85 @@ func TestChatMessageDoesNotExposeUnverifiedQuestionAsFact(t *testing.T) {
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Cognition != "verification_required" || strings.Contains(response.ReplyText, "небезопасная") || !strings.Contains(response.ReplyText, "не буду придумывать") {
|
||||
t.Fatalf("unexpected truth-first response: %#v", response)
|
||||
if response.Cognition != string(cognitive.FinishCompleted) || strings.Contains(response.ReplyText, "небезопасная") || !strings.Contains(response.ReplyText, "продолжаю искать") {
|
||||
t.Fatalf("unexpected research recovery response: %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageUsesLocalAdvisoryForOrdinaryConversationalQuestion(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback"})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/advisories" {
|
||||
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
|
||||
}
|
||||
var request cognitive.GatewayAdvisoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Я рядом. Давай поговорим.", FinishReason: cognitive.FinishCompleted})
|
||||
}))
|
||||
defer cognitiveRuntime.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Как ты?"}`))
|
||||
request.Header.Set("Authorization", "Bearer session-token")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response browserChatResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.ReplyText != "Я рядом. Давай поговорим." || response.Cognition != string(cognitive.FinishCompleted) {
|
||||
t.Fatalf("unexpected conversational response: %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessagePreservesCompletedCoreAnswerInsteadOfSecondAdvisory(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback", PlanningReply: "Готовый ответ ядра."})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("completed core answer must not be replaced by a second advisory")
|
||||
}))
|
||||
defer cognitiveRuntime.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Обычный вопрос"}`))
|
||||
request.Header.Set("Authorization", "Bearer session-token")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
var response browserChatResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.ReplyText != "Готовый ответ ядра." {
|
||||
t.Fatalf("unexpected browser response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,14 @@ func main() {
|
||||
envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"),
|
||||
envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"),
|
||||
envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"),
|
||||
&http.Client{Timeout: 10 * time.Second},
|
||||
// Core chat may spend several seconds on semantic interpretation and
|
||||
// local cognition. Keep the browser gateway alive long enough to return
|
||||
// the completed response instead of converting a successful core 201
|
||||
// into a misleading 502.
|
||||
// A cold local model may need more than one short HTTP round-trip to
|
||||
// load on CPU. Keep the browser gateway aligned with the cognition
|
||||
// deadline instead of turning that wait into a false unavailable reply.
|
||||
&http.Client{Timeout: 15 * time.Minute},
|
||||
envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"),
|
||||
envOr("AGENT_WORKER_URL", "http://agent-worker:8090"),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func TestMultimodalUploadRequiresSessionAndProxiesLocalResult(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer session" {
|
||||
t.Fatalf("unexpected session request: %s %q", r.URL.Path, r.Header.Get("Authorization"))
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"})
|
||||
}))
|
||||
defer core.Close()
|
||||
cognitive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/multimodal/transcribe" || r.Method != http.MethodPost {
|
||||
t.Fatalf("unexpected multimodal request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalTranscribe, Text: "локальный текст", Provider: "local-whisper", ObservedAt: 1})
|
||||
}))
|
||||
defer cognitive.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitive.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
denied := httptest.NewRecorder()
|
||||
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodPost, "/api/multimodal/transcribe", bytes.NewReader(nil)))
|
||||
if denied.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unauthenticated upload status=%d", denied.Code)
|
||||
}
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "voice.wav")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = part.Write([]byte("audio"))
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/multimodal/transcribe", body)
|
||||
request.Header.Set("Authorization", "Bearer session")
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !bytes.Contains(response.Body.Bytes(), []byte("локальный текст")) {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
+148
-17
@@ -7,7 +7,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -26,7 +28,11 @@ import (
|
||||
const (
|
||||
maxRequestBytes = 8 << 10
|
||||
maxAdvisoryRequestBytes = 20 << 10
|
||||
maxResponseBytes = 1 << 20
|
||||
// Chat responses may contain a locally generated image encoded as base64.
|
||||
// Keep the gateway limit aligned with the bounded media transport (32 MiB),
|
||||
// otherwise a successfully generated image is incorrectly surfaced as an
|
||||
// unavailable runtime to the human.
|
||||
maxResponseBytes = 32 << 20
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
@@ -135,6 +141,10 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
g.handleChatMessage(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/multimodal/") {
|
||||
g.handleMultimodal(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/api/fleet/readiness" {
|
||||
g.handleFleetReadiness(w, r)
|
||||
return
|
||||
@@ -214,6 +224,8 @@ type browserChatResponse struct {
|
||||
DelegationID string `json:"delegation_id"`
|
||||
DelegationState string `json:"delegation_state"`
|
||||
ReminderScheduled bool `json:"reminder_scheduled"`
|
||||
GeneratedImageContentType string `json:"generated_image_content_type,omitempty"`
|
||||
GeneratedImageBase64 string `json:"generated_image_base64,omitempty"`
|
||||
}
|
||||
|
||||
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -468,11 +480,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
saved.NeedsCognition = false
|
||||
}
|
||||
}
|
||||
if saved.SpeechAct == interaction.SpeechActQuestion && saved.InformationRequestKind == "" && !saved.ResearchBacked && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired && !asksAttention(browserRequest.Message) {
|
||||
reply = truthfulVerificationRequiredReply()
|
||||
cognition = "verification_required"
|
||||
saved.NeedsCognition = false
|
||||
} else if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
// Core owns the completed planning answer. The browser may ask for a local
|
||||
// advisory only when core explicitly left the turn unanswered; otherwise a
|
||||
// second model pass can replace a grounded result with a weaker one.
|
||||
if saved.NeedsCognition && saved.PlanningReply == "" && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
cognition = "unavailable"
|
||||
requestID, idErr := ids.New()
|
||||
if idErr == nil {
|
||||
@@ -483,7 +494,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
status, err = g.postTrustedJSON(r, g.cognitive, "/v1/advisories", "", payload, &advisory)
|
||||
if err == nil && status == http.StatusOK && validAdvisoryResponse(advisory, trusted) {
|
||||
cognition = string(advisory.FinishReason)
|
||||
if advisory.FinishReason == cognitive.FinishCompleted {
|
||||
if advisory.FinishReason == cognitive.FinishCompleted && (strings.TrimSpace(saved.FallbackText) == "" || !isBoilerplateAdvisory(advisory.AdvisoryText)) {
|
||||
reply = advisory.AdvisoryText
|
||||
}
|
||||
}
|
||||
@@ -511,21 +522,38 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
DelegationID: saved.DelegationID,
|
||||
DelegationState: saved.DelegationState,
|
||||
ReminderScheduled: saved.Reminder != nil,
|
||||
GeneratedImageContentType: saved.GeneratedImageContentType,
|
||||
GeneratedImageBase64: saved.GeneratedImageBase64,
|
||||
})
|
||||
}
|
||||
|
||||
func truthfulVerificationRequiredReply() string {
|
||||
return "Я не могу сейчас подтвердить этот факт по проверенному источнику, поэтому не буду придумывать ответ. Для проверки используйте официальный сайт соответствующей службы или надёжный сервис с текущими данными; когда подключён research-маршрут, Домовой сможет проверить это сам."
|
||||
func isBoilerplateAdvisory(value string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
switch normalized {
|
||||
case "я рядом.", "я рядом", "понял.", "понял", "принял.", "принял":
|
||||
return true
|
||||
default:
|
||||
return strings.Contains(normalized, "я — домов") || strings.Contains(normalized, "я - домов") || strings.Contains(normalized, "меня зовут домов")
|
||||
}
|
||||
}
|
||||
|
||||
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
|
||||
if strings.TrimSpace(saved.ActiveProjectName) == "" {
|
||||
return []cognitive.ContextExcerpt{}
|
||||
assistantName := strings.TrimSpace(saved.AssistantName)
|
||||
if assistantName == "" {
|
||||
assistantName = "Домовой"
|
||||
}
|
||||
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
MaxBytes: 2048,
|
||||
Candidates: []cognitive.ContextCandidate{{
|
||||
candidates := []cognitive.ContextCandidate{{
|
||||
Excerpt: cognitive.ContextExcerpt{
|
||||
ID: "assistant-identity",
|
||||
SourceKind: "assistant-identity-policy",
|
||||
SourceID: "domovoy",
|
||||
Sensitivity: cognitive.SensitivityPublic,
|
||||
Text: "Ты — " + assistantName + ", локальный помощник пользователя. Отвечай от имени «" + assistantName + "». Не называй себя Qwen, Alibaba Cloud, поставщиком модели или внутренним runtime. Если спрашивают имя, скажи: «Меня зовут " + assistantName + ".» Твоя задача — понять смысл запроса, найти путь к цели и довести помощь до полезного конечного результата; не ограничивайся пересказом отказа модели.",
|
||||
},
|
||||
Relevance: 100,
|
||||
}}
|
||||
if strings.TrimSpace(saved.ActiveProjectName) != "" {
|
||||
candidates = append(candidates, cognitive.ContextCandidate{
|
||||
Excerpt: cognitive.ContextExcerpt{
|
||||
ID: cognitive.ContextExcerptID("active-project-focus"),
|
||||
SourceKind: "situation-active-project",
|
||||
@@ -533,8 +561,13 @@ func localChatContext(saved interaction.ChatMessageResponse) []cognitive.Context
|
||||
Sensitivity: cognitive.SensitivityPrivateAllowed,
|
||||
Text: "Текущий проект: " + strings.TrimSpace(saved.ActiveProjectName),
|
||||
},
|
||||
Relevance: 100,
|
||||
}},
|
||||
Relevance: 90,
|
||||
})
|
||||
}
|
||||
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
MaxBytes: 2048,
|
||||
Candidates: candidates,
|
||||
})
|
||||
if err != nil {
|
||||
return []cognitive.ContextExcerpt{}
|
||||
@@ -648,6 +681,104 @@ func (g *gateway) postTrustedJSON(r *http.Request, upstream *url.URL, path, auth
|
||||
return response.StatusCode, nil
|
||||
}
|
||||
|
||||
func (g *gateway) handleMultimodal(w http.ResponseWriter, r *http.Request) {
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
var session identity.CreatorSession
|
||||
status, err := g.getTrustedJSON(r, g.core, "/v1/creator/sessions/current", "", authorization, &session)
|
||||
if err != nil || status != http.StatusOK || session.ActorID == "" || session.PrincipalID == "" {
|
||||
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
const maxMediaBytes = 12 << 20
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxMediaBytes)
|
||||
if err := r.ParseMultipartForm(maxMediaBytes); err != nil {
|
||||
http.Error(w, "media upload is too large or invalid", http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
path := ""
|
||||
switch r.URL.Path {
|
||||
case "/api/multimodal/transcribe":
|
||||
path = "/v1/multimodal/transcribe"
|
||||
case "/api/multimodal/understand":
|
||||
path = "/v1/multimodal/understand"
|
||||
case "/api/multimodal/ocr":
|
||||
path = "/v1/multimodal/ocr"
|
||||
case "/api/multimodal/video-understand":
|
||||
path = "/v1/multimodal/video-understand"
|
||||
case "/api/multimodal/generate-image":
|
||||
path = "/v1/multimodal/generate-image"
|
||||
default:
|
||||
http.Error(w, "multimodal route not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
if r.URL.Path == "/api/multimodal/generate-image" {
|
||||
_ = writer.WriteField("prompt", r.FormValue("prompt"))
|
||||
_ = writer.WriteField("size", r.FormValue("size"))
|
||||
} else {
|
||||
if r.URL.Path == "/api/multimodal/video-understand" {
|
||||
_ = writer.WriteField("instruction", r.FormValue("instruction"))
|
||||
}
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "media file is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(file, maxMediaBytes+1))
|
||||
if err != nil || len(data) == 0 || len(data) > maxMediaBytes {
|
||||
http.Error(w, "media file is invalid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mediaHeader := make(textproto.MIMEHeader)
|
||||
mediaHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, strings.ReplaceAll(header.Filename, `"`, "")))
|
||||
mediaType := header.Header.Get("Content-Type")
|
||||
if mediaType == "" {
|
||||
mediaType = "application/octet-stream"
|
||||
}
|
||||
mediaHeader.Set("Content-Type", mediaType)
|
||||
part, err := writer.CreatePart(mediaHeader)
|
||||
if err != nil {
|
||||
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if _, err := part.Write(data); err != nil {
|
||||
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
http.Error(w, "media upload unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
target := *g.cognitive
|
||||
target.Path = path
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodPost, target.String(), body)
|
||||
if err != nil {
|
||||
http.Error(w, "media capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
response, err := g.client.Do(request)
|
||||
if err != nil {
|
||||
http.Error(w, "media capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, maxResponseBytes))
|
||||
http.Error(w, "media capability unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes))
|
||||
}
|
||||
|
||||
func decodeStrictJSON(reader io.Reader, limit int64, destination any) error {
|
||||
decoder := json.NewDecoder(io.LimitReader(reader, limit+1))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -13,13 +14,43 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
|
||||
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
|
||||
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
|
||||
func TestMaxResponseBytesAcceptsGeneratedImagePayload(t *testing.T) {
|
||||
// A 1024px PNG can exceed the former 1 MiB gateway limit once base64
|
||||
// encoded. The browser gateway must forward that successful core response.
|
||||
payload, err := json.Marshal(interaction.ChatMessageResponse{
|
||||
GeneratedImageContentType: "image/png",
|
||||
GeneratedImageBase64: strings.Repeat("A", 2<<20),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded interaction.ChatMessageResponse
|
||||
if err := decodeStrictJSON(bytes.NewReader(payload), maxResponseBytes, &decoded); err != nil {
|
||||
t.Fatalf("generated image response must fit gateway limit: %v", err)
|
||||
}
|
||||
if len(decoded.GeneratedImageBase64) != 2<<20 {
|
||||
t.Fatalf("generated image payload was truncated: %d", len(decoded.GeneratedImageBase64))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalChatContextIncludesDomovoyIdentityAndOptionalActiveProject(t *testing.T) {
|
||||
context := localChatContext(interaction.ChatMessageResponse{AssistantName: "Фея", ActiveProjectName: "Домовой"})
|
||||
if len(context) != 2 || context[0].SourceID != "domovoy" || !strings.Contains(context[0].Text, "Ты — Фея") || !strings.Contains(context[0].Text, "Меня зовут Фея") || context[1].Text != "Текущий проект: Домовой" {
|
||||
t.Fatalf("unexpected local context: %#v", context)
|
||||
}
|
||||
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
|
||||
t.Fatalf("expected empty context without active project: %#v", empty)
|
||||
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 1 || empty[0].SourceID != "domovoy" {
|
||||
t.Fatalf("identity context must remain available without active project: %#v", empty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoilerplateAdvisoryDoesNotReplaceUsefulFallback(t *testing.T) {
|
||||
for _, value := range []string{"Я рядом", "Понял.", "Принял.", "Я — Домовой, ваш помощник."} {
|
||||
if !isBoilerplateAdvisory(value) {
|
||||
t.Fatalf("%q was not recognized as boilerplate", value)
|
||||
}
|
||||
}
|
||||
if isBoilerplateAdvisory("Жили-были дед и баба.") {
|
||||
t.Fatal("useful creative reply was treated as boilerplate")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.chat-header > .operator-link { flex: 0 0 auto; }
|
||||
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
|
||||
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
|
||||
.generated-image { display: block; max-width: min(100%, 640px); height: auto; margin-top: 12px; border: 1px solid var(--line); }
|
||||
.chat-left { border-right: 1px solid var(--line); }
|
||||
.chat-right { border-left: 1px solid var(--line); }
|
||||
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
|
||||
@@ -75,6 +76,10 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
|
||||
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
|
||||
.composer button { height: 48px; align-self: end; }
|
||||
.composer button:disabled { cursor: wait; filter: saturate(.3); }
|
||||
.media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; }
|
||||
.media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; }
|
||||
.media-tools input { max-width: 150px; font: 10px var(--mono); }
|
||||
.media-tools span { color: var(--signal); }
|
||||
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
|
||||
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
|
||||
|
||||
@@ -103,7 +103,7 @@ function showLogin() {
|
||||
loginView.hidden = false;
|
||||
}
|
||||
|
||||
function appendMessage(kind, text, meta = "") {
|
||||
function appendMessage(kind, text, meta = "", media = null) {
|
||||
const node = document.createElement("div");
|
||||
node.className = `message ${kind}`;
|
||||
node.textContent = text;
|
||||
@@ -112,6 +112,13 @@ function appendMessage(kind, text, meta = "") {
|
||||
small.textContent = meta;
|
||||
node.appendChild(small);
|
||||
}
|
||||
if (media?.data && media?.contentType) {
|
||||
const image = document.createElement("img");
|
||||
image.className = "generated-image";
|
||||
image.alt = text || "Сгенерированное изображение";
|
||||
image.src = `data:${media.contentType};base64,${media.data}`;
|
||||
node.appendChild(image);
|
||||
}
|
||||
messages.appendChild(node);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
localHistory.push({kind, text, meta, at: new Date()});
|
||||
@@ -132,6 +139,16 @@ async function api(path, options = {}) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function uploadMedia(path, file) {
|
||||
const body = new FormData();
|
||||
body.append("file", file, file.name);
|
||||
const headers = {};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const response = await fetch(path, {method: "POST", headers, body});
|
||||
if (!response.ok) throw new ChatApiError(response.status, await response.text());
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function expireSession(message = "") {
|
||||
token = "";
|
||||
conversationID = "";
|
||||
@@ -267,7 +284,7 @@ async function sendChatMessage(text, options = {}) {
|
||||
conversationID = result.conversation_id;
|
||||
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
|
||||
updateActiveFocus(result.active_focus, result.project_provenance);
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result));
|
||||
appendMessage("assistant", result.reply_text || "Сообщение записано.", humanMeta(result), result.generated_image_base64 ? {contentType: result.generated_image_content_type || "image/png", data: result.generated_image_base64} : null);
|
||||
if (/что сейчас требует моего внимания/i.test(clean)) {
|
||||
await loadToday();
|
||||
activateTab("today-panel");
|
||||
@@ -320,6 +337,34 @@ chatForm.addEventListener("submit", async (event) => {
|
||||
await sendChatMessage(text);
|
||||
});
|
||||
|
||||
async function processMedia(input, path, label) {
|
||||
const file = input?.files?.[0];
|
||||
if (!file || !mediaStatus) return;
|
||||
mediaStatus.textContent = `${label} обрабатывается...`;
|
||||
input.value = "";
|
||||
try {
|
||||
const result = await uploadMedia(path, file);
|
||||
const text = String(result.text || "").trim();
|
||||
if (!text) throw new Error("empty media result");
|
||||
mediaStatus.textContent = "Готово";
|
||||
await sendChatMessage(`Пользователь передал ${label.toLowerCase()}. Обработай содержание и ответь по смыслу:\n${text}`);
|
||||
} catch (error) {
|
||||
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
|
||||
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
|
||||
} else {
|
||||
mediaStatus.textContent = `${label} сейчас недоступно.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mediaStatus = document.querySelector("#media-status");
|
||||
const audioInput = document.querySelector("#audio-input");
|
||||
const imageInput = document.querySelector("#image-input");
|
||||
const ocrInput = document.querySelector("#ocr-input");
|
||||
if (audioInput) audioInput.addEventListener("change", () => processMedia(audioInput, "/api/multimodal/transcribe", "Распознанное аудио"));
|
||||
if (imageInput) imageInput.addEventListener("change", () => processMedia(imageInput, "/api/multimodal/understand", "Описание изображения"));
|
||||
if (ocrInput) ocrInput.addEventListener("change", () => processMedia(ocrInput, "/api/multimodal/ocr", "Текст с изображения"));
|
||||
|
||||
for (const tab of navTabs) {
|
||||
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
|
||||
}
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
|
||||
<button id="send" type="submit">Отправить</button>
|
||||
</form>
|
||||
<div class="media-tools" aria-label="Мультимодальные сообщения">
|
||||
<label>Аудио <input id="audio-input" type="file" accept="audio/*"></label>
|
||||
<label>Фото <input id="image-input" type="file" accept="image/*"></label>
|
||||
<label>Текст с фото <input id="ocr-input" type="file" accept="image/*"></label>
|
||||
<span id="media-status" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
<div class="conversation-starters" aria-label="Частые вопросы">
|
||||
<span>Можно начать так:</span>
|
||||
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
|
||||
|
||||
@@ -49,6 +49,9 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
|
||||
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
|
||||
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
|
||||
}
|
||||
if err := validateGenerationTask(task); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
|
||||
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
|
||||
}
|
||||
@@ -58,6 +61,24 @@ func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateGenerationTask(task TaskPacket) error {
|
||||
switch {
|
||||
case task.TaskID == "PRE-GEN2-000":
|
||||
if task.BaseRepoRevision != "origin/main" || task.ArchitectureRevision != "ADR-0019" {
|
||||
return fmt.Errorf("task %s must use origin/main and ADR-0019", task.TaskID)
|
||||
}
|
||||
case task.TaskID == "ARCH-GEN2-000":
|
||||
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ADR-0019" {
|
||||
return fmt.Errorf("task %s must use gen2-integration and ADR-0019", task.TaskID)
|
||||
}
|
||||
case strings.HasPrefix(task.TaskID, "GEN2-"):
|
||||
if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ARCH-GEN2-000" {
|
||||
return fmt.Errorf("task %s must use gen2-integration and ARCH-GEN2-000", task.TaskID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRequiredTests(tests []string) error {
|
||||
joined := strings.Join(tests, "\n")
|
||||
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
|
||||
|
||||
@@ -25,3 +25,42 @@ func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
|
||||
t.Fatalf("delta error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGenerationTaskRequiresGen2BaseAndArchitecture(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task TaskPacket
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "architecture task requires integration base",
|
||||
task: TaskPacket{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
|
||||
want: "gen2-integration",
|
||||
},
|
||||
{
|
||||
name: "implementation task requires accepted architecture root",
|
||||
task: TaskPacket{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
|
||||
want: "ARCH-GEN2-000",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateGenerationTask(tc.task)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("generation validation error = %v, want %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGenerationTaskAcceptsExecutionRoots(t *testing.T) {
|
||||
for _, task := range []TaskPacket{
|
||||
{TaskID: "PRE-GEN2-000", BaseRepoRevision: "origin/main", ArchitectureRevision: "ADR-0019"},
|
||||
{TaskID: "ARCH-GEN2-000", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ADR-0019"},
|
||||
{TaskID: "GEN2-001", BaseRepoRevision: "gen2-integration", ArchitectureRevision: "ARCH-GEN2-000"},
|
||||
} {
|
||||
if err := validateGenerationTask(task); err != nil {
|
||||
t.Fatalf("validateGenerationTask(%+v) = %v", task, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,16 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
searchConnector := researchadapter.SearchConnector{EndpointTemplate: os.Getenv("RESEARCH_SEARCH_URL_TEMPLATE"), AllowedHosts: allowedHosts(os.Getenv("RESEARCH_SEARCH_ALLOWED_HOSTS")), Client: &http.Client{Timeout: 8 * time.Second}, MaxResults: 3}
|
||||
searchConnector := researchadapter.SearchConnector{
|
||||
EndpointTemplate: os.Getenv("RESEARCH_SEARCH_URL_TEMPLATE"),
|
||||
AllowedHosts: allowedHosts(os.Getenv("RESEARCH_SEARCH_ALLOWED_HOSTS")),
|
||||
FallbackEndpoints: []researchadapter.SearchEndpoint{{
|
||||
Template: "https://html.duckduckgo.com/html/?q=%s",
|
||||
AllowedHosts: map[string]struct{}{"html.duckduckgo.com": {}},
|
||||
}},
|
||||
Client: &http.Client{Timeout: 8 * time.Second},
|
||||
MaxResults: 3,
|
||||
}
|
||||
mux.HandleFunc("POST /v1/research/web/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request epistemics.WebSearchRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package capability
|
||||
|
||||
// MultimodalOperation identifies one bounded local media skill. It is kept
|
||||
// separate from general cognition so health, routing and failure evidence do
|
||||
// not collapse into a generic model response.
|
||||
type MultimodalOperation string
|
||||
|
||||
const (
|
||||
MultimodalTranscribe MultimodalOperation = "TRANSCRIBE_AUDIO"
|
||||
MultimodalUnderstand MultimodalOperation = "UNDERSTAND_IMAGE"
|
||||
MultimodalRecognizeOCR MultimodalOperation = "RECOGNIZE_OCR"
|
||||
MultimodalUnderstandVideo MultimodalOperation = "UNDERSTAND_VIDEO"
|
||||
MultimodalGenerateImage MultimodalOperation = "GENERATE_IMAGE"
|
||||
MultimodalGenerateVideo MultimodalOperation = "GENERATE_VIDEO"
|
||||
)
|
||||
|
||||
type MultimodalResponse struct {
|
||||
Operation MultimodalOperation `json:"operation"`
|
||||
Text string `json:"text"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
ObservedAt int64 `json:"observed_at_unix_millis"`
|
||||
ConfidenceNote string `json:"confidence_note,omitempty"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
DataBase64 string `json:"data_base64,omitempty"`
|
||||
}
|
||||
@@ -29,6 +29,12 @@ const (
|
||||
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
|
||||
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
|
||||
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
|
||||
CapabilityKindSpeechToText CapabilityKind = "SPEECH_TO_TEXT"
|
||||
CapabilityKindImageUnderstanding CapabilityKind = "IMAGE_UNDERSTANDING"
|
||||
CapabilityKindOCR CapabilityKind = "OCR"
|
||||
CapabilityKindVideoUnderstanding CapabilityKind = "VIDEO_UNDERSTANDING"
|
||||
CapabilityKindImageGeneration CapabilityKind = "IMAGE_GENERATION"
|
||||
CapabilityKindVideoGeneration CapabilityKind = "VIDEO_GENERATION"
|
||||
)
|
||||
|
||||
// Binding is Capability-owned correctness state. SecretRef is an opaque
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Configuration contract boundary
|
||||
|
||||
Configuration owns typed and versioned `SettingDefinition` and
|
||||
`SettingAssignment` records plus scoped resolution and change provenance.
|
||||
Effective configuration is a derived result. Secret values never enter a
|
||||
setting assignment; only a `SecretRef`/`CredentialRef` crosses the secrets
|
||||
boundary into an authorized adapter or runtime.
|
||||
@@ -0,0 +1,7 @@
|
||||
package configuration
|
||||
|
||||
import "git.cin.su/m/Assistant/platform/ids"
|
||||
|
||||
// SemanticReference is a typed cross-owner reference; settings remain
|
||||
// Configuration-owned records.
|
||||
type SemanticReference = ids.SemanticRef
|
||||
@@ -0,0 +1,7 @@
|
||||
package epistemics
|
||||
|
||||
import "git.cin.su/m/Assistant/platform/ids"
|
||||
|
||||
// SemanticReference identifies evidence-related records without transferring
|
||||
// canonical ownership to Epistemics.
|
||||
type SemanticReference = ids.SemanticRef
|
||||
@@ -27,49 +27,54 @@ type SemanticCandidateSelectionState string
|
||||
type InformationRequestKind 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"
|
||||
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"
|
||||
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
|
||||
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
|
||||
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
|
||||
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
|
||||
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"
|
||||
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
|
||||
InformationRequestLocalCapabilityInventory InformationRequestKind = "LOCAL_CAPABILITY_INVENTORY"
|
||||
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
|
||||
InformationRequestCognitiveCurrentModel InformationRequestKind = "COGNITIVE_CURRENT_MODEL"
|
||||
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
|
||||
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
|
||||
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
|
||||
InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE"
|
||||
InformationRequestResearchFailureExplanation InformationRequestKind = "RESEARCH_FAILURE_EXPLANATION"
|
||||
)
|
||||
|
||||
type Conversation struct {
|
||||
@@ -168,6 +173,7 @@ type ChatMessageResponse struct {
|
||||
InterpretationState InterpretationState
|
||||
NeedsCognition bool
|
||||
FallbackText string
|
||||
AssistantName string
|
||||
ActiveProjectName string
|
||||
ProjectProvenance situation.ProjectResolutionProvenance
|
||||
Clarification ClarificationState
|
||||
@@ -184,4 +190,6 @@ type ChatMessageResponse struct {
|
||||
DelegationID string
|
||||
DelegationState string
|
||||
Reminder *continuity.ProspectiveItem
|
||||
GeneratedImageContentType string
|
||||
GeneratedImageBase64 string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# SELF contract boundary
|
||||
|
||||
SELF owns evidence-backed competence, weakness, dependency, learning, and
|
||||
self-evaluation records. Raw evidence remains Epistemics-owned; capability
|
||||
health remains Capability-owned; tasks and development remain Work-owned;
|
||||
release switching remains Supervisor/runtime-owned.
|
||||
|
||||
SELF is observational/evaluative and is not a mandatory request-path
|
||||
orchestrator or an authority source.
|
||||
@@ -0,0 +1,7 @@
|
||||
package self
|
||||
|
||||
import "git.cin.su/m/Assistant/platform/ids"
|
||||
|
||||
// SemanticReference lets SELF cite evidence and capabilities without owning
|
||||
// their canonical state.
|
||||
type SemanticReference = ids.SemanticRef
|
||||
@@ -0,0 +1,7 @@
|
||||
package situation
|
||||
|
||||
import "git.cin.su/m/Assistant/platform/ids"
|
||||
|
||||
// SemanticReference keeps Situation references transport-neutral while
|
||||
// ContextScope semantics remain owned by Situation.
|
||||
type SemanticReference = ids.SemanticRef
|
||||
@@ -0,0 +1,9 @@
|
||||
# World contract boundary
|
||||
|
||||
World owns typed, versioned referents and vocabulary: `WorldObject`, kind and
|
||||
facet/relation definitions, aliases, identity-resolution decisions, and
|
||||
merge/split lineage. It does not own observations, claims, beliefs, or the
|
||||
truth of values about a referent; those remain Epistemics-owned evidence.
|
||||
|
||||
Cross-owner references are typed semantic references. They do not create
|
||||
cross-owner foreign keys, joins, writes, or a generic `Entity(any)` API.
|
||||
@@ -0,0 +1,270 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/platform/clock"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
// SemanticReference is the transport envelope used by World contracts. It is
|
||||
// a reference only; it does not expose another owner's storage.
|
||||
type SemanticReference = ids.SemanticRef
|
||||
|
||||
type PrincipalRealm string
|
||||
|
||||
const (
|
||||
RealmPersonal PrincipalRealm = "PERSONAL"
|
||||
RealmShared PrincipalRealm = "SHARED"
|
||||
RealmPublic PrincipalRealm = "PUBLIC"
|
||||
RealmSystem PrincipalRealm = "SYSTEM"
|
||||
)
|
||||
|
||||
type SensitivityClass string
|
||||
|
||||
const (
|
||||
SensitivityPublic SensitivityClass = "PUBLIC"
|
||||
SensitivityPrivate SensitivityClass = "PRIVATE"
|
||||
SensitivityRestricted SensitivityClass = "RESTRICTED"
|
||||
SensitivitySecretReference SensitivityClass = "SECRET_REFERENCE"
|
||||
)
|
||||
|
||||
type ValueTypeKind string
|
||||
|
||||
const (
|
||||
ValueString ValueTypeKind = "STRING"
|
||||
ValueInteger ValueTypeKind = "INTEGER"
|
||||
ValueDecimal ValueTypeKind = "DECIMAL"
|
||||
ValueBoolean ValueTypeKind = "BOOLEAN"
|
||||
ValueTime ValueTypeKind = "TIME"
|
||||
ValueDuration ValueTypeKind = "DURATION"
|
||||
ValueEnum ValueTypeKind = "ENUM"
|
||||
ValueReference ValueTypeKind = "REFERENCE"
|
||||
ValueBinaryRef ValueTypeKind = "BINARY_REF"
|
||||
ValueList ValueTypeKind = "LIST"
|
||||
ValueStruct ValueTypeKind = "STRUCT"
|
||||
)
|
||||
|
||||
type ValueField struct {
|
||||
Name string
|
||||
Type ValueTypeSpec
|
||||
}
|
||||
|
||||
type ValueTypeSpec struct {
|
||||
Kind ValueTypeKind
|
||||
Element *ValueTypeSpec
|
||||
Fields []ValueField
|
||||
EnumValues []string
|
||||
ReferenceKind string
|
||||
}
|
||||
|
||||
type WorldObjectID ids.StableID
|
||||
type KindDefinitionID ids.StableID
|
||||
type WorldAliasID ids.StableID
|
||||
type FacetDefinitionID ids.StableID
|
||||
type RelationDefinitionID ids.StableID
|
||||
type ResolutionDecisionID ids.StableID
|
||||
type LineageID ids.StableID
|
||||
|
||||
type LifecycleState string
|
||||
|
||||
const (
|
||||
LifecycleActive LifecycleState = "ACTIVE"
|
||||
LifecycleMerged LifecycleState = "MERGED"
|
||||
LifecycleSplit LifecycleState = "SPLIT"
|
||||
LifecycleRetired LifecycleState = "RETIRED"
|
||||
LifecycleUnknownIdentity LifecycleState = "UNKNOWN_IDENTITY"
|
||||
)
|
||||
|
||||
type WorldObject struct {
|
||||
ID WorldObjectID
|
||||
PrincipalID identity.PrincipalID
|
||||
KindDefinitionRef SemanticReference
|
||||
KindRevision ids.Revision
|
||||
Lifecycle LifecycleState
|
||||
CreatedAt clock.Instant
|
||||
}
|
||||
|
||||
type WorldObjectKindDefinition struct {
|
||||
ID KindDefinitionID
|
||||
KindKey string
|
||||
Revision ids.Revision
|
||||
DisplayName string
|
||||
ParentKindRef *SemanticReference
|
||||
Lifecycle LifecycleState
|
||||
DefinitionDigest string
|
||||
RecordedAt clock.Instant
|
||||
}
|
||||
|
||||
type WorldAlias struct {
|
||||
ID WorldAliasID
|
||||
PrincipalID identity.PrincipalID
|
||||
ObjectID WorldObjectID
|
||||
Alias string
|
||||
RecordedAt clock.Instant
|
||||
}
|
||||
|
||||
type FacetDefinition struct {
|
||||
ID FacetDefinitionID
|
||||
KindKey string
|
||||
Revision ids.Revision
|
||||
FacetKey string
|
||||
ValueType ValueTypeSpec
|
||||
DefinitionDigest string
|
||||
RecordedAt clock.Instant
|
||||
}
|
||||
|
||||
type RelationDefinition struct {
|
||||
ID RelationDefinitionID
|
||||
KindKey string
|
||||
Revision ids.Revision
|
||||
RelationKey string
|
||||
TargetKindKey string
|
||||
DefinitionDigest string
|
||||
RecordedAt clock.Instant
|
||||
}
|
||||
|
||||
type ResolutionDecision string
|
||||
|
||||
const (
|
||||
ResolutionSame ResolutionDecision = "SAME"
|
||||
ResolutionDistinct ResolutionDecision = "DISTINCT"
|
||||
ResolutionMerge ResolutionDecision = "MERGE"
|
||||
ResolutionSplit ResolutionDecision = "SPLIT"
|
||||
ResolutionUnresolved ResolutionDecision = "UNRESOLVED"
|
||||
)
|
||||
|
||||
type IdentityResolutionCandidate struct {
|
||||
ObjectRef SemanticReference
|
||||
Evidence []SemanticReference
|
||||
Confidence int64
|
||||
}
|
||||
|
||||
type IdentityResolutionDecisionRecord struct {
|
||||
ID ResolutionDecisionID
|
||||
PrincipalID identity.PrincipalID
|
||||
Candidates []IdentityResolutionCandidate
|
||||
Decision ResolutionDecision
|
||||
Evidence []SemanticReference
|
||||
ConfidenceMillis int64
|
||||
DecidedAt clock.Instant
|
||||
Revision ids.Revision
|
||||
}
|
||||
|
||||
type WorldLineage struct {
|
||||
ID LineageID
|
||||
Operation ResolutionDecision
|
||||
SourceRefs []SemanticReference
|
||||
SuccessorRefs []SemanticReference
|
||||
RecordedAt clock.Instant
|
||||
}
|
||||
|
||||
func (w WorldObject) Validate() error {
|
||||
if w.ID == "" || !w.KindDefinitionRef.Valid() || !w.KindRevision.Valid() || !validLifecycle(w.Lifecycle) || !w.CreatedAt.Valid() {
|
||||
return fmt.Errorf("world object is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k WorldObjectKindDefinition) Validate() error {
|
||||
if k.ID == "" || k.KindKey == "" || !k.Revision.Valid() || k.DisplayName == "" || !validLifecycle(k.Lifecycle) || k.DefinitionDigest == "" || !k.RecordedAt.Valid() {
|
||||
return fmt.Errorf("world kind definition is incomplete")
|
||||
}
|
||||
if k.ParentKindRef != nil && !k.ParentKindRef.Valid() {
|
||||
return fmt.Errorf("world kind parent reference is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a WorldAlias) Validate() error {
|
||||
if a.ID == "" || a.ObjectID == "" || a.Alias == "" || !a.RecordedAt.Valid() {
|
||||
return fmt.Errorf("world alias is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FacetDefinition) Validate() error {
|
||||
if f.ID == "" || f.KindKey == "" || !f.Revision.Valid() || f.FacetKey == "" || f.DefinitionDigest == "" || !f.RecordedAt.Valid() {
|
||||
return fmt.Errorf("facet definition is incomplete")
|
||||
}
|
||||
return f.ValueType.Validate()
|
||||
}
|
||||
|
||||
func (r RelationDefinition) Validate() error {
|
||||
if r.ID == "" || r.KindKey == "" || !r.Revision.Valid() || r.RelationKey == "" || r.TargetKindKey == "" || r.DefinitionDigest == "" || !r.RecordedAt.Valid() {
|
||||
return fmt.Errorf("relation definition is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d IdentityResolutionDecisionRecord) Validate() error {
|
||||
if d.ID == "" || !validDecision(d.Decision) || len(d.Candidates) == 0 || d.ConfidenceMillis < 0 || !d.DecidedAt.Valid() || !d.Revision.Valid() {
|
||||
return fmt.Errorf("identity resolution decision is incomplete")
|
||||
}
|
||||
for _, candidate := range d.Candidates {
|
||||
if !candidate.ObjectRef.Valid() || candidate.Confidence < 0 || candidate.Confidence > 1000 {
|
||||
return fmt.Errorf("identity resolution candidate is invalid")
|
||||
}
|
||||
for _, evidence := range candidate.Evidence {
|
||||
if !evidence.Valid() {
|
||||
return fmt.Errorf("identity resolution evidence reference is invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l WorldLineage) Validate() error {
|
||||
if l.ID == "" || (l.Operation != ResolutionMerge && l.Operation != ResolutionSplit) || len(l.SourceRefs) == 0 || len(l.SuccessorRefs) == 0 || !l.RecordedAt.Valid() {
|
||||
return fmt.Errorf("world lineage is incomplete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validLifecycle(value LifecycleState) bool {
|
||||
return value == LifecycleActive || value == LifecycleMerged || value == LifecycleSplit || value == LifecycleRetired || value == LifecycleUnknownIdentity
|
||||
}
|
||||
|
||||
func validDecision(value ResolutionDecision) bool {
|
||||
return value == ResolutionSame || value == ResolutionDistinct || value == ResolutionMerge || value == ResolutionSplit || value == ResolutionUnresolved
|
||||
}
|
||||
|
||||
func (s ValueTypeSpec) Validate() error {
|
||||
if s.Kind == "" {
|
||||
return fmt.Errorf("value type kind is required")
|
||||
}
|
||||
switch s.Kind {
|
||||
case ValueList:
|
||||
if s.Element == nil {
|
||||
return fmt.Errorf("list element type is required")
|
||||
}
|
||||
return s.Element.Validate()
|
||||
case ValueStruct:
|
||||
if len(s.Fields) == 0 {
|
||||
return fmt.Errorf("struct fields are required")
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, field := range s.Fields {
|
||||
if field.Name == "" {
|
||||
return fmt.Errorf("struct field name is required")
|
||||
}
|
||||
if _, ok := seen[field.Name]; ok {
|
||||
return fmt.Errorf("duplicate struct field %q", field.Name)
|
||||
}
|
||||
seen[field.Name] = struct{}{}
|
||||
if err := field.Type.Validate(); err != nil {
|
||||
return fmt.Errorf("field %s: %w", field.Name, err)
|
||||
}
|
||||
}
|
||||
case ValueEnum:
|
||||
if len(s.EnumValues) == 0 {
|
||||
return fmt.Errorf("enum values are required")
|
||||
}
|
||||
case ValueReference:
|
||||
if s.ReferenceKind == "" {
|
||||
return fmt.Errorf("reference kind is required")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package world
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValueTypeSpecValidatesTypedRecursiveShape(t *testing.T) {
|
||||
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "name", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "refs", Type: ValueTypeSpec{Kind: ValueList, Element: &ValueTypeSpec{Kind: ValueReference, ReferenceKind: "world.object"}}}}}
|
||||
if err := spec.Validate(); err != nil {
|
||||
t.Fatalf("ValueTypeSpec.Validate() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueTypeSpecRejectsDuplicateFields(t *testing.T) {
|
||||
spec := ValueTypeSpec{Kind: ValueStruct, Fields: []ValueField{{Name: "same", Type: ValueTypeSpec{Kind: ValueString}}, {Name: "same", Type: ValueTypeSpec{Kind: ValueString}}}}
|
||||
if err := spec.Validate(); err == nil {
|
||||
t.Fatal("duplicate fields were accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/platform/clock"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
func TestWorldObjectValidationKeepsReferentSeparateFromClaims(t *testing.T) {
|
||||
object := WorldObject{ID: "object-1", KindDefinitionRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "kind", StableID: "kind-1"}, KindRevision: 1, Lifecycle: LifecycleActive, CreatedAt: clock.Instant(1)}
|
||||
if err := object.Validate(); err != nil {
|
||||
t.Fatalf("WorldObject.Validate() = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityResolutionRequiresEvidenceBackedCandidates(t *testing.T) {
|
||||
decision := IdentityResolutionDecisionRecord{ID: "decision-1", Decision: ResolutionSame, Candidates: []IdentityResolutionCandidate{{ObjectRef: ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "object", StableID: "object-1"}, Confidence: 900}}, DecidedAt: clock.Instant(1), Revision: 1}
|
||||
if err := decision.Validate(); err != nil {
|
||||
t.Fatalf("IdentityResolutionDecisionRecord.Validate() = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS world;
|
||||
SET ROLE world_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.kind_definitions (
|
||||
kind_definition_id TEXT PRIMARY KEY,
|
||||
kind_key TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||
display_name TEXT NOT NULL,
|
||||
parent_owner_namespace TEXT NOT NULL DEFAULT '',
|
||||
parent_semantic_kind TEXT NOT NULL DEFAULT '',
|
||||
parent_stable_id TEXT NOT NULL DEFAULT '',
|
||||
parent_revision BIGINT,
|
||||
lifecycle TEXT NOT NULL,
|
||||
definition_digest TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
|
||||
UNIQUE (kind_key, revision)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.objects (
|
||||
world_object_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT,
|
||||
kind_owner_namespace TEXT NOT NULL,
|
||||
kind_semantic_kind TEXT NOT NULL,
|
||||
kind_stable_id TEXT NOT NULL,
|
||||
kind_revision BIGINT,
|
||||
lifecycle TEXT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.aliases (
|
||||
world_alias_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT,
|
||||
world_object_id TEXT NOT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.facet_definitions (
|
||||
facet_definition_id TEXT PRIMARY KEY,
|
||||
kind_key TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||
facet_key TEXT NOT NULL,
|
||||
value_type JSONB NOT NULL,
|
||||
definition_digest TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
|
||||
UNIQUE (kind_key, facet_key, revision)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.relation_definitions (
|
||||
relation_definition_id TEXT PRIMARY KEY,
|
||||
kind_key TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||
relation_key TEXT NOT NULL,
|
||||
target_kind_key TEXT NOT NULL,
|
||||
definition_digest TEXT NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0),
|
||||
UNIQUE (kind_key, relation_key, revision)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.identity_resolution_decisions (
|
||||
resolution_decision_id TEXT PRIMARY KEY,
|
||||
principal_id TEXT,
|
||||
candidates JSONB NOT NULL,
|
||||
decision TEXT NOT NULL,
|
||||
evidence JSONB NOT NULL,
|
||||
confidence_millis BIGINT NOT NULL CHECK (confidence_millis >= 0),
|
||||
decided_at_unix_millis BIGINT NOT NULL CHECK (decided_at_unix_millis > 0),
|
||||
revision BIGINT NOT NULL CHECK (revision > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS world.lineage (
|
||||
lineage_id TEXT PRIMARY KEY,
|
||||
operation TEXT NOT NULL,
|
||||
source_refs JSONB NOT NULL,
|
||||
successor_refs JSONB NOT NULL,
|
||||
recorded_at_unix_millis BIGINT NOT NULL CHECK (recorded_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -102,8 +102,11 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://docker.cin.su:18082/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_TIMEOUT: ${LOCAL_MEDIUM_PROVIDER_TIMEOUT:-120s}
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
ports: ["19083:8083"]
|
||||
|
||||
|
||||
@@ -258,6 +258,9 @@ services:
|
||||
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
|
||||
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
|
||||
LOCAL_IMAGE_PROVIDER_KIND: ${LOCAL_IMAGE_PROVIDER_KIND:-comfyui}
|
||||
LOCAL_IMAGE_PROVIDER_URL: ${LOCAL_IMAGE_PROVIDER_URL:-http://comfyui:8188}
|
||||
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
depends_on: [core-runtime, cognitive-lifeboat]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000291-mobile-acceptance-evidence.json",
|
||||
"task_id": "DEV-000291",
|
||||
"task_revision": 1
|
||||
}
|
||||
"task_file": "dev/tasks/GEN2-003-add-narrow-world-owner-and-typed-vocabulary.json",
|
||||
"task_revision": 1,
|
||||
"task_id": "GEN2-003"
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"task_id": "ARCH-GEN2-000",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0019",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Сделать Gen2 официальным архитектурным поколением, а не внешним пожеланием.",
|
||||
"requirement_ids": [
|
||||
"REQ-ARCH-GEN2-000-GOAL",
|
||||
"REQ-ARCH-GEN2-000-BOUNDARY",
|
||||
"REQ-ARCH-GEN2-000-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"architecture/**",
|
||||
"development/**",
|
||||
"docs/**",
|
||||
"AGENTS.md",
|
||||
"cmd/devctl/**",
|
||||
"ci/architecture/**",
|
||||
".gitea/workflows/ci.yml",
|
||||
"dev/**"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "Architecture/Governance. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"нет runtime writer change",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"V1 governance -> Gen2 documents accepted -> architecture authority/registry updated -> later implementation tasks become legal"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No runtime schema change. This task adopts Gen2 canonical documents/governance only."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: revert architecture adoption до начала schema migration; baseline tag остаётся неизменным",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M0; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Executable task id is ARCH-GEN2-000 because current devctl accepts ARCH-* as an architecture_revision root; this corresponds exactly to GEN2-000 in the Master Plan.",
|
||||
"Depends on: PRE-GEN2-000."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"ARCH-GEN2-000-SCN-01: внести DOMOVOY_CONSTITUTION, GEN2_TARGET_ARCHITECTURE, GEN2_CANONICAL_DATA_MODEL, GEN2_MIGRATION_MAP и GEN2_MASTER_PLAN в repo",
|
||||
"ARCH-GEN2-000-SCN-02: принять ADR/architecture-generation decision, который разрешает новые owners World/Configuration/SELF и Gen2 migrations",
|
||||
"ARCH-GEN2-000-SCN-03: обновить authority order архитектурных документов",
|
||||
"ARCH-GEN2-000-SCN-04: закрыть/заморозить DEV-000295 как V1 baseline/audit evidence или явно supersede его Gen2 program",
|
||||
"ARCH-GEN2-000-SCN-05: добавить Gen2 task validation в devctl",
|
||||
"ARCH-GEN2-000-SCN-06: добавить FAST/MILESTONE/FULL test profiles или эквивалентную profile metadata без тяжёлого переписывания CI",
|
||||
"ARCH-GEN2-000-SCN-07: внести GEN2_EXECUTION_CONTRACT, GEN2_TASK_INDEX и все последующие GEN2 task packets; сделать их canonical execution program",
|
||||
"ARCH-GEN2-000-EXIT: Codex перед каждым Gen2 task обязан читать canonical Gen2 docs; ordinary V1 task больше не имеет права менять Gen2 architecture."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/devctl/...",
|
||||
"FAST: go vet ./cmd/devctl/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"FAST architecture: go run ./cmd/devctl check-architecture"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"task_id": "DEV-000292",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "HEAD",
|
||||
"risk_tier": "E",
|
||||
"intent": "Provide an explicitly isolated development/test runtime profile for Domovoy so local model, speech, image, OCR, Docker and diagnostic paths are usable without changing production behavior.",
|
||||
"requirement_ids": ["isolated development runtime", "local multimodal model access", "development diagnostics"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["interaction", "core-runtime", "cognitive-worker", "creator-web", "deployment", "development-governance"],
|
||||
"paths": [
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"cmd/research-runtime/**",
|
||||
"adapters/models/**",
|
||||
"adapters/research/**",
|
||||
"contracts/cognitive/**",
|
||||
"modules/interaction/domain/**",
|
||||
"deploy/docker-compose.dev.yml",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000292-development-mode-runtime-access.json",
|
||||
"docs/evidence/**",
|
||||
"docs/PROJECT_STATUS.md"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 1,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["existing cognitive gateway", "existing local model HTTP APIs", "existing dev compose network"],
|
||||
"may_change": ["dev-only modality and diagnostic wiring; no production contract change"]
|
||||
},
|
||||
"semantic_owner": "development-governance owns the isolated dev profile; cognitive-worker owns model capability routing",
|
||||
"canonical_state": ["existing dev databases and Docker volumes only"],
|
||||
"state_transitions": ["production-only restriction -> explicit dev-only capability switch"],
|
||||
"side_effects_and_privileges": ["dev network and Docker diagnostics only", "no production credentials", "no secret logging", "no production external mutation"],
|
||||
"persistence_and_migrations": ["dev volumes only; no production migration"],
|
||||
"failure_retry_restart": ["dev services remain independently restartable", "unsupported modality stays explicitly unavailable"],
|
||||
"assumptions": ["the developer starts the profile separately from production", "test credentials are disposable and never reused in production"],
|
||||
"acceptance_scenarios": ["dev profile starts independently", "text, speech, image and OCR routes are discoverable or report a precise unavailable state", "diagnostic endpoints are reachable inside the dev network", "meaningful chat requests prefer cognitive interpretation over token reflexes", "stable reflexes are sampled periodically for cognitive drift and a disagreement is observable as a cognitive candidate", "production compose remains unchanged", "no secret value appears in source or logs"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "docker compose -f deploy/docker-compose.dev.yml config", "git diff --check", "dev runtime health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"task_id": "DEV-000293",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0019",
|
||||
"base_repo_revision": "origin/main",
|
||||
"risk_tier": "E",
|
||||
"intent": "Authorize a bounded read-only site-browser capability for dynamic web pages so Domovoy can observe rendered product data, including prices, without granting account access or external mutation authority.",
|
||||
"requirement_ids": ["bounded browser observation", "dynamic web research", "price evidence extraction"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["capability", "epistemics", "core-runtime", "research"],
|
||||
"paths": [
|
||||
"architecture/adrs/ADR-0014-assistant-presence-attention-human-surface.md",
|
||||
"architecture/adrs/ADR-0015-human-response-continuation-semantics.md",
|
||||
"architecture/adrs/ADR-0019-bounded-site-browser-observation.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000293-site-browser-capability.json",
|
||||
"docs/PROJECT_STATUS.md"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["existing capability registry", "existing epistemics observation/provenance", "existing research adapter boundary"],
|
||||
"may_change": ["architecture allowance for bounded read-only browser observation"]
|
||||
},
|
||||
"semantic_owner": "capability owns availability and limits; epistemics owns observations and provenance",
|
||||
"canonical_state": ["no new canonical state; browser output remains an observation/artifact projection"],
|
||||
"state_transitions": ["search-only observation -> bounded rendered-page observation -> source-backed answer"],
|
||||
"side_effects_and_privileges": ["read-only HTTPS/browser egress only", "no cookies, account sessions, credentials, downloads, uploads, form submission or external mutation"],
|
||||
"persistence_and_migrations": ["no database migration"],
|
||||
"failure_retry_restart": ["timeouts and blocked pages remain partial/unknown observations", "no blind retries that can duplicate external effects"],
|
||||
"assumptions": ["browser runtime is replaceable and isolated from canonical runtimes", "site allowlist and navigation policy are server-owned"],
|
||||
"acceptance_scenarios": ["dynamic price page can be observed when allowlisted", "401/challenge/timeout is reported as unavailable evidence", "browser cannot submit credentials or mutate a site", "source URL, observed time and coverage remain attached", "existing search-only route remains unchanged when browser capability is unavailable", "production behavior stays read-only"],
|
||||
"required_tests": ["architecture preflight", "check-diff", "ADR consistency", "go test ./...", "go vet ./...", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"task_id": "DEV-000294",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0019",
|
||||
"base_repo_revision": "origin/main",
|
||||
"risk_tier": "E",
|
||||
"intent": "Route creative requests by meaning: render simple geometric sketches deterministically and send rich creative requests to cognition without web research or truth-evidence fallback.",
|
||||
"requirement_ids": ["creative conversation", "no irrelevant web research", "short deterministic drawing"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["interaction", "core-runtime"],
|
||||
"paths": [
|
||||
"cmd/core-runtime/**",
|
||||
"modules/interaction/domain/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000294-creative-chat-routing.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": ["existing interaction semantic candidate", "existing cognitive gateway", "existing web research route"],
|
||||
"may_change": ["creative capability hints and core route selection"]
|
||||
},
|
||||
"semantic_owner": "interaction owns meaning classification; core-runtime owns route orchestration",
|
||||
"canonical_state": ["existing utterance and candidate history only"],
|
||||
"state_transitions": ["creative request -> deterministic sketch or cognitive conversation route"],
|
||||
"side_effects_and_privileges": ["no new privilege or external mutation"],
|
||||
"persistence_and_migrations": ["no migration"],
|
||||
"failure_retry_restart": ["creative cognition failure reports unavailable cognition; never falls back to web research"],
|
||||
"assumptions": ["simple circle rendering is a text-safe projection; richer images remain a separate capability"],
|
||||
"acceptance_scenarios": ["нарисуй круг returns a circle and no source links", "расскажи сказку о колобке uses cognition and no web route", "ordinary weather and price requests keep web research", "cognitive review cannot replace deterministic circle"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"task_id": "DEV-000295",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ADR-0019",
|
||||
"base_repo_revision": "origin/main",
|
||||
"risk_tier": "E",
|
||||
"intent": "Провести сценарный аудит бытового общения Домового, добавить устойчивую маршрутизацию умений и проверку конечного ответа без монолитного оркестратора.",
|
||||
"requirement_ids": [
|
||||
"REQ-HOUSEHOLD-001",
|
||||
"REQ-HOUSEHOLD-002",
|
||||
"REQ-HOUSEHOLD-003",
|
||||
"REQ-HOUSEHOLD-004"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["interaction", "capability", "epistemics", "situation", "work", "continuity", "cognitive-worker", "creator-web", "multimodal-adapters"],
|
||||
"paths": [
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/creator-web/**",
|
||||
"adapters/multimodal/**",
|
||||
"modules/interaction/**",
|
||||
"modules/capability/**",
|
||||
"modules/epistemics/**",
|
||||
"modules/situation/**",
|
||||
"modules/work/**",
|
||||
"modules/continuity/**",
|
||||
"contracts/interaction/**",
|
||||
"contracts/capability/**",
|
||||
"deploy/docker-compose.prod.yml",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000295-household-scenario-audit.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"architecture/**",
|
||||
"security/**",
|
||||
"adapters/models/**",
|
||||
"deploy/images/**",
|
||||
"migrations/**",
|
||||
"go.mod",
|
||||
"go.sum"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 1,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.authority@v1",
|
||||
"contracts.capability@v1",
|
||||
"contracts.cognitive@v1",
|
||||
"contracts.interaction@v1",
|
||||
"contracts.work@v1"
|
||||
],
|
||||
"may_change": ["multimodal capability kinds and internal speech/image/OCR transport contracts; no canonical persistence contract"]
|
||||
},
|
||||
"semantic_owner": "interaction owns meaning classification; capability owns capability health and selection; cognitive-worker/adapters own provider transport; creator-web owns browser transport; existing semantic owners retain canonical state",
|
||||
"canonical_state": ["existing Interpretation, CapabilityResolution, Situation, Goal/Task and ProspectiveItem only"],
|
||||
"state_transitions": ["request -> meaning -> bounded capability route -> result/clarification/degraded answer -> verification evidence"],
|
||||
"side_effects_and_privileges": ["Scenario tests are read-only/fake by default; multimodal requests send user-provided media only to configured local speech/vision services; real reminders, websites, messages and code changes remain behind existing Commit/Executor and authority boundaries."],
|
||||
"persistence_and_migrations": ["No new tables, migrations or events; verify existing durable memory, situation, goal/task and continuity paths."],
|
||||
"failure_retry_restart": ["Each scenario distinguishes unavailable, partial and unknown outcome; retries must not duplicate external effects; restart restores context and commitments from canonical state."],
|
||||
"acceptance_scenarios": [
|
||||
"SCN-HH-001 identity/name/preference continuity",
|
||||
"SCN-HH-002 location/weather/time/price current-data research",
|
||||
"SCN-HH-003 travel/nearby-place clarification and result synthesis",
|
||||
"SCN-HH-004 multi-action request decomposition and agent assignment",
|
||||
"SCN-HH-005 reminder/goal/task lifecycle and follow-up",
|
||||
"SCN-HH-006 speech/image/OCR capability routing and degraded mode",
|
||||
"SCN-HH-007 creative conversation without web-search leakage",
|
||||
"SCN-HH-008 unavailable/partial/contradictory tool and model responses",
|
||||
"SCN-HH-009 short natural answer and no internal protocol leakage",
|
||||
"SCN-HH-010 authority/confirmation/unknown-outcome safety"
|
||||
],
|
||||
"required_tests": [
|
||||
"go test",
|
||||
"go vet",
|
||||
"preflight",
|
||||
"check-diff",
|
||||
"check-architecture",
|
||||
"independent",
|
||||
"git diff",
|
||||
"git diff --check",
|
||||
"unit",
|
||||
"semantic-routing",
|
||||
"scenario-contract",
|
||||
"multimodal-adapter",
|
||||
"multimodal-proxy",
|
||||
"multimodal-production-smoke",
|
||||
"degraded-mode",
|
||||
"restart-recovery",
|
||||
"architecture",
|
||||
"go-test ./...",
|
||||
"go vet ./...",
|
||||
"production smoke on docker.cin.su"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY"
|
||||
],
|
||||
"assumptions": [
|
||||
"Локальный Docker запрещён; production verification выполняется только на docker.cin.su.",
|
||||
"Провайдеры и browser session могут быть недоступны, поэтому тесты обязаны проверять полезный degraded ответ.",
|
||||
"Новые умения добавляются отдельными capability/adapter/route компонентами, а не единым монолитным оркестратором."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"task_id": "GEN2-001",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Формально закрепить owners, forbidden dependencies и migration law before tables.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-001-GOAL",
|
||||
"REQ-GEN2-001-BOUNDARY",
|
||||
"REQ-GEN2-001-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"architecture/**",
|
||||
"contracts/world/**",
|
||||
"contracts/configuration/**",
|
||||
"contracts/self/**",
|
||||
"cmd/devctl/**",
|
||||
"ci/architecture/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-001-accept-gen2-owner-registry-and-dependency-rules.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 32,
|
||||
"new_canonical_owners": 3,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 3
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-001 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Architecture Registry. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"нет data writer change",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"V1 governance -> Gen2 documents accepted -> architecture authority/registry updated -> later implementation tasks become legal"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: architecture revert возможен только до первого зависимого Gen2 task; далее через новый ADR",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M0; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: ARCH-GEN2-000."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-001-SCN-01: принять owner registry Gen2",
|
||||
"GEN2-001-SCN-02: развести AuthorityScope и ContextScope",
|
||||
"GEN2-001-SCN-03: зафиксировать World != truth owner, SELF != orchestrator, Memory != owner",
|
||||
"GEN2-001-SCN-04: зафиксировать single canonical writer, no cross-owner SQL/FK/JOIN by default",
|
||||
"GEN2-001-SCN-05: зафиксировать Supervisor как runtime layer, не semantic owner",
|
||||
"GEN2-001-SCN-06: добавить architecture checks для новых границ там, где это возможно дешёво",
|
||||
"GEN2-001-EXIT: все последующие task packets могут однозначно указать current_owner/target_owner."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/devctl/...",
|
||||
"FAST: go vet ./cmd/devctl/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"FAST architecture: go run ./cmd/devctl check-architecture"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"task_id": "GEN2-002",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Дать Gen2 стабильные ID/revision/time/SemanticRef/PrincipalRealm/Sensitivity primitives без giant shared domain model.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-002-GOAL",
|
||||
"REQ-GEN2-002-BOUNDARY",
|
||||
"REQ-GEN2-002-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"world",
|
||||
"situation",
|
||||
"epistemics",
|
||||
"configuration",
|
||||
"self"
|
||||
],
|
||||
"paths": [
|
||||
"platform/ids/**",
|
||||
"platform/clock/**",
|
||||
"platform/errors/**",
|
||||
"contracts/world/**",
|
||||
"contracts/situation/**",
|
||||
"contracts/epistemics/**",
|
||||
"contracts/configuration/**",
|
||||
"contracts/self/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-002-introduce-shared-gen2-data-primitives-without-shared-domain-semantics.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.authority@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-002 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Platform/Contracts. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"существующие owners остаются writers",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: существующие owners остаются writers"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: additive primitives; old code не удалять",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M1; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-001."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-002-SCN-01: определить StableID/Revision primitives",
|
||||
"GEN2-002-SCN-02: определить SemanticRef как cross-owner reference без прямого DB ownership",
|
||||
"GEN2-002-SCN-03: определить time primitives и data realm/sensitivity primitives",
|
||||
"GEN2-002-SCN-04: реализовать typed value algebra там, где он действительно общий технический контракт",
|
||||
"GEN2-002-SCN-05: не переносить domain meaning в platform/common",
|
||||
"GEN2-002-EXIT: World/Focus/Epistemics/Configuration могут ссылаться на одинаковые технические refs без shared semantic owner."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./platform/ids/... ./contracts/world/... ./contracts/situation/... ./contracts/epistemics/... ./contracts/configuration/... ./contracts/self/...",
|
||||
"FAST: go vet ./platform/ids/... ./contracts/world/... ./contracts/situation/... ./contracts/epistemics/... ./contracts/configuration/... ./contracts/self/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"task_id": "GEN2-003",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Создать устойчивые referents/aliases/types/facet definitions/relation definitions, не присваивая World владение истиной значений.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-003-GOAL",
|
||||
"REQ-GEN2-003-BOUNDARY",
|
||||
"REQ-GEN2-003-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"world"
|
||||
],
|
||||
"paths": [
|
||||
"modules/world/**",
|
||||
"contracts/world/**",
|
||||
"adapters/postgres/world_repository.go",
|
||||
"adapters/postgres/world_*.go",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-003-add-narrow-world-owner-and-typed-vocabulary.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.authority@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-003 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "World (new). Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"before: project/situation/ad-hoc identifiers; after: World canonical only for referent vocabulary, not facts",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: project/situation/ad-hoc identifiers; after: World canonical only for referent vocabulary, not facts"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: V1 identifiers остаются compatibility refs; World tables additive; switch readers back без удаления World evidence",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M1; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-002."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-003-SCN-01: создать WorldObject stable identity",
|
||||
"GEN2-003-SCN-02: WorldObjectKindDefinition",
|
||||
"GEN2-003-SCN-03: WorldAlias",
|
||||
"GEN2-003-SCN-04: FacetDefinition и RelationDefinition с versioned typed schemas",
|
||||
"GEN2-003-SCN-05: identity-resolution candidate/decision structures",
|
||||
"GEN2-003-SCN-06: обеспечить split/merge lineage",
|
||||
"GEN2-003-SCN-07: начать с минимального набора объектов, необходимых для Focus/Scope migration",
|
||||
"GEN2-003-EXIT: одинаковые имена разных компаний/баз могут иметь разные stable referents; World не хранит claim «какое значение истинно»."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/world/... ./contracts/world/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/world/... ./contracts/world/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"task_id": "GEN2-004",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Сделать Focus многомерным: person/org/system/database/repository/version/activity/goal/task/entities, а Project оставить лишь возможной гранью.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-004-GOAL",
|
||||
"REQ-GEN2-004-BOUNDARY",
|
||||
"REQ-GEN2-004-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"situation",
|
||||
"interaction"
|
||||
],
|
||||
"paths": [
|
||||
"modules/situation/**",
|
||||
"contracts/situation/**",
|
||||
"adapters/postgres/situation_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"cmd/core-runtime/**",
|
||||
"modules/interaction/**",
|
||||
"contracts/interaction/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-004-replace-project-centric-focus-with-contextscope-focusframe-focusstack.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.authority@v1",
|
||||
"contracts.world@gen2",
|
||||
"contracts.work@v1",
|
||||
"contracts.capability@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-004 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Situation. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"before: V1 situation/project focus; after: Situation FocusFrame/Transition; ActiveProjectFocus becomes projection/compatibility",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: V1 situation/project focus; after: Situation FocusFrame/Transition; ActiveProjectFocus becomes projection/compatibility"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: feature switch возвращает V1 read path; Gen2 transitions сохраняются",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M1; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-003."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-004-SCN-01: добавить ContextScope RoleKey/ScopeValue/MatchPolicy",
|
||||
"GEN2-004-SCN-02: добавить FocusFrame stable ID + revision",
|
||||
"GEN2-004-SCN-03: добавить FocusTransition append-only ledger",
|
||||
"GEN2-004-SCN-04: добавить FocusStack/parent/suspended/resume semantics",
|
||||
"GEN2-004-SCN-05: реализовать explicit focus command и inferred focus как разные evidence paths",
|
||||
"GEN2-004-SCN-06: построить compatibility projection ActiveProjectFocus из FocusFrame",
|
||||
"GEN2-004-SCN-07: переключить core/context readers на Focus V2 постепенно",
|
||||
"GEN2-004-EXIT: короткое отвлечение может push/pop focus; Company A/UNF и Company B/UNF не смешиваются по текущему scope."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/situation/... ./contracts/situation/... ./modules/interaction/... ./cmd/core-runtime/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/situation/... ./contracts/situation/... ./modules/interaction/... ./cmd/core-runtime/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"task_id": "GEN2-005",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Заложить безопасное same_as?/merge/split и привязку Focus к конкретной версии/окружению.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-005-GOAL",
|
||||
"REQ-GEN2-005-BOUNDARY",
|
||||
"REQ-GEN2-005-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"world",
|
||||
"situation"
|
||||
],
|
||||
"paths": [
|
||||
"modules/world/**",
|
||||
"contracts/world/**",
|
||||
"modules/situation/**",
|
||||
"contracts/situation/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-005-world-identity-resolution-and-version-environment-bindings.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.world@gen2",
|
||||
"contracts.situation@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-005 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "World + Situation contracts, no truth ownership. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"World writer только identity/referent resolution",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: World writer только identity/referent resolution"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: resolution decisions versioned/reversible; no destructive identity merge",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M1; test profile: MILESTONE.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-004."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-005-SCN-01: реализовать identity resolution candidate с confidence/evidence refs",
|
||||
"GEN2-005-SCN-02: не объединять автоматически неоднозначных людей/системы",
|
||||
"GEN2-005-SCN-03: добавить reversible merge/split lineage",
|
||||
"GEN2-005-SCN-04: добавить version/environment reference primitives, используемые позже Epistemics freshness",
|
||||
"GEN2-005-EXIT: «Иван Telegram» и «Иван CRM» могут быть связаны с уверенностью без необратимого склеивания; две УНФ не путаются."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/world/... ./modules/situation/...",
|
||||
"FAST: go vet ./modules/world/... ./modules/situation/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"MILESTONE M1: go test ./modules/world/... ./modules/situation/... ./modules/interaction/... ./cmd/core-runtime/...",
|
||||
"MILESTONE M1: run focused scope/focus company-A-vs-company-B and focus-stack regressions; do not run full repository suite."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"task_id": "GEN2-006",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Убрать зависимость от denormalized truth_bundles как конечной модели истины и создать переоцениваемую канонику evidence/claim/belief.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-006-GOAL",
|
||||
"REQ-GEN2-006-BOUNDARY",
|
||||
"REQ-GEN2-006-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"epistemics"
|
||||
],
|
||||
"paths": [
|
||||
"modules/epistemics/**",
|
||||
"contracts/epistemics/**",
|
||||
"adapters/postgres/epistemics_repository.go",
|
||||
"adapters/research/**",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-006-normalize-epistemics-evidence-propositions-claims-and-belief-assessments.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.artifact@v1",
|
||||
"contracts.world@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-006 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"before: V1 truth bundle paths; after: normalized Epistemics for new Gen2 knowledge; old bundle writer disabled at cutover or made projection",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: V1 truth bundle paths; after: normalized Epistemics for new Gen2 knowledge; old bundle writer disabled at cutover or made projection"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: read switch назад на V1 bundle; normalized evidence не удалять",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M2; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-005."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-006-SCN-01: добавить Source/SourceSnapshot/SourceExcerpt",
|
||||
"GEN2-006-SCN-02: добавить immutable Observation",
|
||||
"GEN2-006-SCN-03: добавить Proposition/Claim",
|
||||
"GEN2-006-SCN-04: добавить versioned BeliefAssessment",
|
||||
"GEN2-006-SCN-05: backfill существующих truth/research данных с lineage",
|
||||
"GEN2-006-SCN-06: оставить truth_bundles read compatibility до позднего retirement",
|
||||
"GEN2-006-SCN-07: обеспечить возможность новой модели переоценить старый Claim без переписывания Observation",
|
||||
"GEN2-006-EXIT: источник, наблюдение, утверждение и текущая оценка уверенности раздельны и traceable."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/epistemics/... ./contracts/epistemics/... ./adapters/research/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/epistemics/... ./contracts/epistemics/... ./adapters/research/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"task_id": "GEN2-007",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Сделать старение знания контекстным: прошлогодняя УНФ может быть stale для live-debug и полезной аналогией без live-доступа.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-007-GOAL",
|
||||
"REQ-GEN2-007-BOUNDARY",
|
||||
"REQ-GEN2-007-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"epistemics",
|
||||
"work"
|
||||
],
|
||||
"paths": [
|
||||
"modules/epistemics/**",
|
||||
"contracts/epistemics/**",
|
||||
"contracts/work/**",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-007-general-freshness-volatility-temporal-applicability-and-revalidation.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.work@v1",
|
||||
"contracts.world@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-007 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Epistemics writes observation/verification metadata; freshness status computed",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Epistemics writes observation/verification metadata; freshness status computed"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: metadata additive; old readers ignore it",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M2; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-006."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-007-SCN-01: добавить TemporalApplicability",
|
||||
"GEN2-007-SCN-02: VolatilitySpec/decay class",
|
||||
"GEN2-007-SCN-03: last_verified/observed metadata",
|
||||
"GEN2-007-SCN-04: VersionBinding",
|
||||
"GEN2-007-SCN-05: TaskFreshnessRequirement",
|
||||
"GEN2-007-SCN-06: вычисляемый FreshnessAssessment вместо permanent `stale` flag",
|
||||
"GEN2-007-SCN-07: active revalidation request/response contract",
|
||||
"GEN2-007-SCN-08: fallback semantics: memory-only solution должен быть явно маркирован как непроверенный текущими данными",
|
||||
"GEN2-007-EXIT: один и тот же Claim получает разную пригодность в зависимости от task freshness/risk."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/epistemics/... ./contracts/epistemics/... ./contracts/work/...",
|
||||
"FAST: go vet ./modules/epistemics/... ./contracts/epistemics/... ./contracts/work/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"task_id": "GEN2-008",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Сохранять не только «что сработало», но problem/attempt/why/result/why failed/conditions/evidence.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-008-GOAL",
|
||||
"REQ-GEN2-008-BOUNDARY",
|
||||
"REQ-GEN2-008-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"epistemics",
|
||||
"artifact"
|
||||
],
|
||||
"paths": [
|
||||
"modules/epistemics/**",
|
||||
"contracts/epistemics/**",
|
||||
"adapters/postgres/epistemics_repository.go",
|
||||
"contracts/artifact/**",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-008-promote-episode-and-experience-v2-including-negative-experience.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.artifact@v1",
|
||||
"contracts.world@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-008 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Epistemics remains canonical Experience writer; Episode projection derived",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Epistemics remains canonical Experience writer; Episode projection derived"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: старые Experience records остаются валидны; Episode projection rebuildable",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M2; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-007."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-008-SCN-01: расширить ExperienceRecord V1 без потери старых IDs/evidence",
|
||||
"GEN2-008-SCN-02: сделать success/failure first-class",
|
||||
"GEN2-008-SCN-03: добавить conditions/applicability/version bindings",
|
||||
"GEN2-008-SCN-04: создать Episode как derived/rebuildable episodic projection",
|
||||
"GEN2-008-SCN-05: связать user correction с ReflectionCandidate/Experience",
|
||||
"GEN2-008-SCN-06: добавить anti-pattern: failure under conditions != forever wrong",
|
||||
"GEN2-008-EXIT: retrieval позже может отдельно запросить «что уже пробовали и почему не сработало»."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/epistemics/... ./contracts/epistemics/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/epistemics/... ./contracts/epistemics/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"task_id": "GEN2-009",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Дать многоканальный retrieval по canonical owners без единой memory_items каноники.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-009-GOAL",
|
||||
"REQ-GEN2-009-BOUNDARY",
|
||||
"REQ-GEN2-009-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"runtime/retrieval/**",
|
||||
"contracts/cognitive/**",
|
||||
"adapters/postgres/*retrieval*.go",
|
||||
"adapters/postgres/*search*.go",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-009-build-memory-retrieval-fabric-as-rebuildable-projections.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.interaction@v1",
|
||||
"contracts.world@gen2",
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.work@v1",
|
||||
"contracts.continuity@v1",
|
||||
"contracts.capability@v1",
|
||||
"contracts.configuration@gen2",
|
||||
"contracts.self@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-009 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "No canonical Memory owner; projection layer. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"canonical writers unchanged; retrieval index only projection writer",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: canonical writers unchanged; retrieval index only projection writer"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: drop/rebuild projection and use V1 memory retrieval",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M2; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-008."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-009-SCN-01: создать RetrievalDocument projection с source SemanticRef",
|
||||
"GEN2-009-SCN-02: projection builders из Interaction/World/Epistemics/Work/Continuity/Capability/Configuration/SELF",
|
||||
"GEN2-009-SCN-03: жёсткий ContextScope/realm/version filter before similarity",
|
||||
"GEN2-009-SCN-04: lanes: semantic, entity/graph-like refs, temporal, goal/commitment, positive experience, negative experience, contradiction, skill",
|
||||
"GEN2-009-SCN-05: ranking signals: relevance, freshness, confidence, salience, scope distance, cost",
|
||||
"GEN2-009-SCN-06: hot/warm/cold/archive retrieval policy как operational storage concern, не truth",
|
||||
"GEN2-009-SCN-07: projection versioning/rebuild command",
|
||||
"GEN2-009-EXIT: vector index можно полностью перестроить без изменения canonical memory; cross-company leak blocked before embedding rank."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./runtime/retrieval/... ./contracts/cognitive/... ./adapters/postgres",
|
||||
"FAST: go vet ./runtime/retrieval/... ./contracts/cognitive/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"task_id": "GEN2-010",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Формировать минимально достаточный model context под текущий Task/Focus/Model/Risk/Freshness и уметь дозапрашивать память во время reasoning.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-010-GOAL",
|
||||
"REQ-GEN2-010-BOUNDARY",
|
||||
"REQ-GEN2-010-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"contracts/cognitive/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/core-runtime/**",
|
||||
"runtime/retrieval/**",
|
||||
"tests/context_compiler/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-010-context-compiler-v2-with-budgets-coverage-and-iterative-retrieval.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.cognitive@v1",
|
||||
"contracts.situation@gen2",
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.capability@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-010 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Cognitive contract/read path, no canonical owner. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"none; read/compile only",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: none; read/compile only"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: feature switch to Context Compiler V1; V2 trace/projections retained",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M2; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-009."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-010-SCN-01: ContextCompileRequest: task/focus/scope/model/token budget/freshness/risk",
|
||||
"GEN2-010-SCN-02: candidate lanes + deterministic scope filtering",
|
||||
"GEN2-010-SCN-03: positive+negative experience balancing",
|
||||
"GEN2-010-SCN-04: contradiction pass",
|
||||
"GEN2-010-SCN-05: coverage check: не упущены ли critical commitments/known failures",
|
||||
"GEN2-010-SCN-06: ContextBundle с explicit token allocation",
|
||||
"GEN2-010-SCN-07: ContextAssemblyTrace: почему элемент включён/исключён",
|
||||
"GEN2-010-SCN-08: iterative memory search tool для model reasoning",
|
||||
"GEN2-010-SCN-09: маленькой локальной модели давать меньше и точнее, а не просто урезать хвост",
|
||||
"GEN2-010-EXIT: локальная модель получает релевантный пакет без любимого цвета в 1C-debug; нужные прошлые грабли попадают в context."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./contracts/cognitive/... ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/retrieval/... ./tests/context_compiler/...",
|
||||
"FAST: go vet ./contracts/cognitive/... ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/retrieval/... ./tests/context_compiler/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"task_id": "GEN2-011",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Перестать использовать ad-hoc PersonalMemory ContextText как долгосрочную основу памяти, не теряя utterance history.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-011-GOAL",
|
||||
"REQ-GEN2-011-BOUNDARY",
|
||||
"REQ-GEN2-011-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction"
|
||||
],
|
||||
"paths": [
|
||||
"modules/interaction/**",
|
||||
"contracts/interaction/**",
|
||||
"adapters/postgres/interaction_repository.go",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"runtime/retrieval/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-011-cut-personalmemory-v1-to-compatibility-fallback.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.interaction@v1",
|
||||
"contracts.cognitive@gen2",
|
||||
"contracts.situation@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-011 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Interaction -> retrieval fabric reads multiple owners. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"utterance writer unchanged; PersonalMemory derived writer frozen; retrieval projection primary",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: utterance writer unchanged; PersonalMemory derived writer frozen; retrieval projection primary"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: switch reads back to PersonalMemory; no canonical loss",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M2; test profile: MILESTONE.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-010."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-011-SCN-01: оставить HumanUtterance append-only canonical history",
|
||||
"GEN2-011-SCN-02: построить Gen2 retrieval из этой истории",
|
||||
"GEN2-011-SCN-03: сравнить V1 PersonalMemory vs Gen2 context on representative cases",
|
||||
"GEN2-011-SCN-04: переключить primary read на Gen2",
|
||||
"GEN2-011-SCN-05: оставить PersonalMemory read fallback до M6",
|
||||
"GEN2-011-SCN-06: запретить новый semantic growth V1 PersonalMemory",
|
||||
"GEN2-011-EXIT: PersonalMemory больше не определяет архитектуру новых features."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/interaction/... ./cmd/core-runtime/... ./cmd/cognitive-worker/... ./runtime/retrieval/...",
|
||||
"FAST: go vet ./modules/interaction/... ./cmd/core-runtime/... ./cmd/cognitive-worker/... ./runtime/retrieval/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"MILESTONE M2: go test ./modules/epistemics/... ./modules/interaction/... ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./runtime/retrieval/... ./tests/context_compiler/...",
|
||||
"MILESTONE M2: run freshness, negative-experience, contradiction, scope-isolation and PersonalMemory-fallback regressions only."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"task_id": "GEN2-012",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Убрать ordinary runtime settings из ENV и дать Skills/capabilities возможность объявлять новые typed scoped settings без глобальной schema rewrite.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-012-GOAL",
|
||||
"REQ-GEN2-012-BOUNDARY",
|
||||
"REQ-GEN2-012-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"configuration"
|
||||
],
|
||||
"paths": [
|
||||
"modules/configuration/**",
|
||||
"contracts/configuration/**",
|
||||
"adapters/postgres/configuration_repository.go",
|
||||
"platform/secrets/**",
|
||||
"database/migrations/*.sql",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"adapters/models/**",
|
||||
"adapters/research/**",
|
||||
"adapters/multimodal/**",
|
||||
"deploy/docker-compose.prod.yml",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-012-add-typed-scoped-versioned-configuration-owner-and-bootstrap-boundary.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 1,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.world@gen2",
|
||||
"contracts.secrets@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-012 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Configuration (new). Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"before: ENV/files/ad-hoc DB; after: Configuration for migrated non-bootstrap settings",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: before: ENV/files/ad-hoc DB; after: Configuration for migrated non-bootstrap settings"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass.",
|
||||
"Deploy/release surface changes are allowed only for the bounded purpose of this packet; production promotion requires the packet's stated evidence and rollback path."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: per-consumer fallback to old read path; Configuration values retained",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M3; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-011."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-012-SCN-01: inventory всех ENV/config consumers",
|
||||
"GEN2-012-SCN-02: создать SettingDefinition stable identity + schema/version/owner/type/default/validation",
|
||||
"GEN2-012-SCN-03: SettingAssignment + ScopeSelector",
|
||||
"GEN2-012-SCN-04: deterministic inheritance/resolution",
|
||||
"GEN2-012-SCN-05: EffectiveConfiguration trace",
|
||||
"GEN2-012-SCN-06: SecretRef only for credentials",
|
||||
"GEN2-012-SCN-07: определить минимальный bootstrap, который может остаться вне Configuration",
|
||||
"GEN2-012-SCN-08: импортировать non-secret текущие значения",
|
||||
"GEN2-012-SCN-09: shadow compare old vs resolved configuration",
|
||||
"GEN2-012-SCN-10: cutover consumers по одному, Commit runtime последним из критических",
|
||||
"GEN2-012-EXIT: можно сказать «для Skill X в Company A значение Y», а Company B получает другое значение без новых ENV vars/columns."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/configuration/... ./contracts/configuration/... ./platform/secrets/... ./cmd/core-runtime/... ./adapters/models/... ./adapters/research/...",
|
||||
"FAST: go vet ./modules/configuration/... ./contracts/configuration/... ./platform/secrets/... ./cmd/core-runtime/... ./adapters/models/... ./adapters/research/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"task_id": "GEN2-013",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Сделать Skill долговечным обучаемым способом решения, а не только профилем capability.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-013-GOAL",
|
||||
"REQ-GEN2-013-BOUNDARY",
|
||||
"REQ-GEN2-013-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"capability",
|
||||
"work"
|
||||
],
|
||||
"paths": [
|
||||
"modules/capability/**",
|
||||
"contracts/capability/**",
|
||||
"adapters/postgres/capability_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"modules/work/**",
|
||||
"contracts/work/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-013-migrate-skill-v1-to-skill-v2-with-applicability-versioning-and-evidence.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.capability@v1",
|
||||
"contracts.work@v1",
|
||||
"contracts.epistemics@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-013 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Capability. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Capability remains Skill canonical writer",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Capability remains Skill canonical writer"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: V1 skill view/projected compatibility; revisions additive",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M3; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-012."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-013-SCN-01: сохранить stable Skill identity",
|
||||
"GEN2-013-SCN-02: ввести revisions",
|
||||
"GEN2-013-SCN-03: ApplicabilitySpec с Scope/version/conditions",
|
||||
"GEN2-013-SCN-04: связать positive/negative Experience evidence",
|
||||
"GEN2-013-SCN-05: validation/promotion evidence",
|
||||
"GEN2-013-SCN-06: known failure conditions",
|
||||
"GEN2-013-SCN-07: quality history",
|
||||
"GEN2-013-SCN-08: dependency refs",
|
||||
"GEN2-013-SCN-09: сохранить совместимость с существующими SkillProfile/ReflectionCandidate",
|
||||
"GEN2-013-EXIT: успешное решение может стать Candidate/Validated/Active Skill и знает, где он применим."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/capability/... ./contracts/capability/... ./modules/work/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/capability/... ./contracts/capability/... ./modules/work/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"task_id": "GEN2-014",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Дать быстрый локальный путь для проверенных повторяемых задач без закостенения.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-014-GOAL",
|
||||
"REQ-GEN2-014-BOUNDARY",
|
||||
"REQ-GEN2-014-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"capability"
|
||||
],
|
||||
"paths": [
|
||||
"modules/capability/**",
|
||||
"contracts/capability/**",
|
||||
"adapters/postgres/capability_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"cmd/cognitive-worker/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-014-add-explicit-reflex-lifecycle-shadow-reasoning-and-revalidation.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.capability@gen2",
|
||||
"contracts.cognitive@gen2",
|
||||
"contracts.epistemics@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-014 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Capability. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Capability writes Reflex lifecycle; routing reads only ACTIVE applicable reflex",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: Capability writes Reflex lifecycle; routing reads only ACTIVE applicable reflex"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: disable reflex feature switch; route returns to Skill/model",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M3; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-013."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-014-SCN-01: Reflex stable ID/revisions",
|
||||
"GEN2-014-SCN-02: link Reflex -> originating Skill/evidence",
|
||||
"GEN2-014-SCN-03: applicability/dependencies",
|
||||
"GEN2-014-SCN-04: ShadowPolicy/RevalidationPolicy",
|
||||
"GEN2-014-SCN-05: fallback/escalation Reflex -> Skill -> reasoning",
|
||||
"GEN2-014-SCN-06: drift/stale detection",
|
||||
"GEN2-014-SCN-07: candidate/active/suspended/retired states",
|
||||
"GEN2-014-SCN-08: не считать один успешный run достаточным для promotion",
|
||||
"GEN2-014-EXIT: частая стабильная задача может выполняться быстро локально и автоматически вернуться к reasoning при сомнении."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/capability/... ./contracts/capability/... ./cmd/cognitive-worker/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/capability/... ./contracts/capability/... ./cmd/cognitive-worker/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"task_id": "GEN2-015",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Домовой должен знать, что умеет, при каких условиях, где ошибается и что изменилось — без права самому объявить себя хорошим.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-015-GOAL",
|
||||
"REQ-GEN2-015-BOUNDARY",
|
||||
"REQ-GEN2-015-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"self"
|
||||
],
|
||||
"paths": [
|
||||
"modules/self/**",
|
||||
"contracts/self/**",
|
||||
"adapters/postgres/self_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-015-add-evidence-backed-self-foundation-and-learning-ledger.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.capability@gen2",
|
||||
"contracts.work@v1",
|
||||
"contracts.epistemics@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-015 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "SELF (new). Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"SELF writes derived evidence-backed self assessments only",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: SELF writes derived evidence-backed self assessments only"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: SELF can be disabled; no mutation path depends on it initially",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M3; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-014."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-015-SCN-01: CompetenceAssessment from outcomes/tests/task evaluations",
|
||||
"GEN2-015-SCN-02: WeaknessPattern",
|
||||
"GEN2-015-SCN-03: DependencyAssessment",
|
||||
"GEN2-015-SCN-04: ImprovementNeed",
|
||||
"GEN2-015-SCN-05: LearningLedgerEntry",
|
||||
"GEN2-015-SCN-06: SelfCompositionSnapshot",
|
||||
"GEN2-015-SCN-07: provenance каждого self-assessment",
|
||||
"GEN2-015-SCN-08: запрет arbitrary `competence=1.0` от Skill",
|
||||
"GEN2-015-SCN-09: первый режим SELF только observe/evaluate/propose",
|
||||
"GEN2-015-EXIT: система отличает «я разучился» от «зависимость IFCM временно недоступна» и хранит собственные повторяемые weakness patterns."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/self/... ./contracts/self/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/self/... ./contracts/self/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"task_id": "GEN2-016",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Выбирать минимально достаточный интеллект и доказуемо сравнивать модели/skills на реальных задачах владельца.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-016-GOAL",
|
||||
"REQ-GEN2-016-BOUNDARY",
|
||||
"REQ-GEN2-016-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"capability",
|
||||
"work",
|
||||
"self"
|
||||
],
|
||||
"paths": [
|
||||
"modules/capability/**",
|
||||
"contracts/capability/**",
|
||||
"modules/work/**",
|
||||
"contracts/work/**",
|
||||
"modules/self/**",
|
||||
"contracts/self/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-016-intelligence-strategy-resource-cost-policy-and-personal-benchmark.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.capability@gen2",
|
||||
"contracts.work@gen2",
|
||||
"contracts.self@gen2",
|
||||
"contracts.cognitive@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-016 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Capability + Work evaluation; SELF observes. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Capability/Work existing evaluation writers extended; SELF reads assessments",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Capability/Work existing evaluation writers extended; SELF reads assessments"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: routing returns to V1 selection; evidence retained",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M3; test profile: FULL.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-015."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-016-SCN-01: formalize path: rule/reflex/skill/small local/strong local/external/research/team",
|
||||
"GEN2-016-SCN-02: input signals: required quality, latency preference, cost budget, risk, freshness, cost of error",
|
||||
"GEN2-016-SCN-03: создать Personal Benchmark dataset refs из реальных approved task cases",
|
||||
"GEN2-016-SCN-04: определить Goal/Preference evaluation profile: owner intent → long-term goals → current task → contextual preferences",
|
||||
"GEN2-016-SCN-05: inferred preferences хранить как evidence-backed scoped hypotheses, а не вечные глобальные настройки",
|
||||
"GEN2-016-SCN-06: сохранять decision-regret evidence: мог ли Домовой с доступными тогда данными выбрать лучше",
|
||||
"GEN2-016-SCN-07: сохранять task-specific evaluation, user correction, latency/cost/retry evidence",
|
||||
"GEN2-016-SCN-08: не сводить качество к одной метрике и не оптимизироваться под согласие владельца",
|
||||
"GEN2-016-SCN-09: добавить exploration/shadow для low-risk paths",
|
||||
"GEN2-016-SCN-10: новая модель не получает automatic promotion только из-за public benchmark",
|
||||
"GEN2-016-EXIT: простая задача не запускает команду из 20 агентов; сложная high-risk задача может эскалировать глубину reasoning."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/capability/... ./modules/work/... ./modules/self/... ./cmd/cognitive-worker/...",
|
||||
"FAST: go vet ./modules/capability/... ./modules/work/... ./modules/self/... ./cmd/cognitive-worker/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"FULL: go test ./...",
|
||||
"FULL: go vet ./...",
|
||||
"FULL M3: architecture + migration + configuration + skill/reflex + SELF benchmark regressions."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"task_id": "GEN2-017",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "«Разбирайся хоть сутки» должно быть durable process с checkpoint, а не один длинный LLM request.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-017-GOAL",
|
||||
"REQ-GEN2-017-BOUNDARY",
|
||||
"REQ-GEN2-017-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"continuity"
|
||||
],
|
||||
"paths": [
|
||||
"modules/work/**",
|
||||
"contracts/work/**",
|
||||
"adapters/postgres/work_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"cmd/work-graph-runtime/**",
|
||||
"cmd/research-runtime/**",
|
||||
"modules/continuity/**",
|
||||
"contracts/continuity/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-017-extend-work-into-durable-longrunningwork-investigation.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.work@v1",
|
||||
"contracts.continuity@v1",
|
||||
"contracts.capability@gen2",
|
||||
"contracts.epistemics@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-017 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Work. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Work canonical writer for long-running job state; Continuity only future relevance",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Work canonical writer for long-running job state; Continuity only future relevance"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: old Work Graph path remains for normal tasks; new jobs can be paused/read-only",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M4; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-016."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-017-SCN-01: LongRunningWork/Investigation state",
|
||||
"GEN2-017-SCN-02: objective, hypotheses, attempts, findings, blockers, next step",
|
||||
"GEN2-017-SCN-03: budget: time/cost/model/resource",
|
||||
"GEN2-017-SCN-04: checkpoint/resume/preempt semantics",
|
||||
"GEN2-017-SCN-05: model/provider replacement between attempts",
|
||||
"GEN2-017-SCN-06: stop condition «решение не найдено в бюджете» с сохранением проверенных вариантов",
|
||||
"GEN2-017-SCN-07: Continuity integration для wake/attention without second scheduler",
|
||||
"GEN2-017-EXIT: долгая задача переживает restart и продолжает с next step, не повторяя уже проверенные попытки."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/work/... ./modules/continuity/... ./cmd/work-graph-runtime/... ./cmd/research-runtime/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/work/... ./modules/continuity/... ./cmd/work-graph-runtime/... ./cmd/research-runtime/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"task_id": "GEN2-018",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Домовой может смотреть на задачу как архитектор/программист/директор/дизайнер или собрать временную команду без roleplay и без постоянного staff.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-018-GOAL",
|
||||
"REQ-GEN2-018-BOUNDARY",
|
||||
"REQ-GEN2-018-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"capability"
|
||||
],
|
||||
"paths": [
|
||||
"modules/work/**",
|
||||
"contracts/work/**",
|
||||
"modules/capability/**",
|
||||
"contracts/capability/**",
|
||||
"cmd/work-graph-runtime/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-018-add-perspective-and-dynamic-specialist-team-semantics-on-existing-work-g.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.work@gen2",
|
||||
"contracts.capability@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-018 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Work. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Work graph remains owner; no separate Agent Memory owner",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Work graph remains owner; no separate Agent Memory owner"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: disable perspective/team routing; tasks execute single path",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M4; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-017."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-018-SCN-01: PerspectiveSpec = objectives/methods/knowledge profile/evaluation criteria",
|
||||
"GEN2-018-SCN-02: dynamic specialist assembly from Perspective + Focus + Skills + tools",
|
||||
"GEN2-018-SCN-03: work decomposition by independent subtasks",
|
||||
"GEN2-018-SCN-04: structured findings/evidence/risks/artifacts",
|
||||
"GEN2-018-SCN-05: critic/adversarial perspective optional",
|
||||
"GEN2-018-SCN-06: criteria when team is justified vs single reasoning path",
|
||||
"GEN2-018-SCN-07: не передавать каждому агенту весь глобальный context",
|
||||
"GEN2-018-EXIT: «спроектируй систему» может вызвать несколько независимых перспектив; «исправь функцию» остаётся одной задачей."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/work/... ./modules/capability/... ./cmd/work-graph-runtime/...",
|
||||
"FAST: go vet ./modules/work/... ./modules/capability/... ./cmd/work-graph-runtime/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"task_id": "GEN2-019",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Оценивать последствия, обратимость, cost-of-error и mitigation до значимого действия, но не превращать систему в «нельзя».",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-019-GOAL",
|
||||
"REQ-GEN2-019-BOUNDARY",
|
||||
"REQ-GEN2-019-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"authority"
|
||||
],
|
||||
"paths": [
|
||||
"modules/work/**",
|
||||
"contracts/work/**",
|
||||
"contracts/authority/**",
|
||||
"modules/authority/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-019-add-consequence-risk-assessment-without-artificial-capability-blacklist.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.work@gen2",
|
||||
"contracts.authority@v1",
|
||||
"contracts.commit@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-019 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Work assessment feeding Authority/Commit. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Work writes assessment; Authority remains grant decision owner",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: Work writes assessment; Authority remains grant decision owner"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass.",
|
||||
"Risk assessment may recommend mitigations but must not invent a blanket capability blacklist or override authenticated owner intent."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: risk assessment advisory disable; strict existing Authority unchanged",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M4; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-018."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-019-SCN-01: ConsequenceAssessment",
|
||||
"GEN2-019-SCN-02: risk dimensions: impact, reversibility, uncertainty, scope, recoverability",
|
||||
"GEN2-019-SCN-03: mitigation: backup/snapshot/dry-run/canary/extra confirmation",
|
||||
"GEN2-019-SCN-04: clarification policy based on uncertainty × cost_of_wrong_action × irreversibility",
|
||||
"GEN2-019-SCN-05: owner may explicitly accept high risk after warning",
|
||||
"GEN2-019-SCN-06: consequence result becomes input Authority/Commit, не replacement",
|
||||
"GEN2-019-EXIT: Домовой предупреждает о последствиях и предлагает rollback plan, но не самовольно запрещает владельцу разрешённое действие."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/work/... ./modules/authority/...",
|
||||
"FAST: go vet ./modules/work/... ./modules/authority/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"task_id": "GEN2-020",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Не считать «API 200» доказательством того, что пользовательская цель достигнута.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-020-GOAL",
|
||||
"REQ-GEN2-020-BOUNDARY",
|
||||
"REQ-GEN2-020-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"commit",
|
||||
"epistemics",
|
||||
"continuity"
|
||||
],
|
||||
"paths": [
|
||||
"modules/commit/**",
|
||||
"contracts/commit/**",
|
||||
"modules/epistemics/**",
|
||||
"contracts/epistemics/**",
|
||||
"modules/continuity/**",
|
||||
"contracts/continuity/**",
|
||||
"adapters/postgres/commit_repository.go",
|
||||
"adapters/postgres/epistemics_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-020-separate-execution-receipt-from-verified-world-outcome.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.commit@v1",
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.continuity@v1",
|
||||
"contracts.work@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-020 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Commit + Epistemics. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Commit writer unchanged for execution facts; Epistemics writes world assessment",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Commit writer unchanged for execution facts; Epistemics writes world assessment"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: old Commit status still readable; new assessments additive",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M4; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-019."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-020-SCN-01: ExecutionReceipt остаётся Commit-owned факт попытки",
|
||||
"GEN2-020-SCN-02: WorldOutcomeAssessment становится Epistemics-owned verification",
|
||||
"GEN2-020-SCN-03: связать expected effect / observation / evidence",
|
||||
"GEN2-020-SCN-04: поддержать UNKNOWN/PARTIAL/CONTRADICTED outcome",
|
||||
"GEN2-020-SCN-05: не выполнять destructive retry только потому, что verification отсутствует",
|
||||
"GEN2-020-SCN-06: Continuity может создать follow-up/attention при неизвестном Outcome",
|
||||
"GEN2-020-EXIT: ответ «выполнено» возможен только когда семантика операции это позволяет; неизвестность честно отражается."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/commit/... ./modules/epistemics/... ./modules/continuity/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/commit/... ./modules/epistemics/... ./modules/continuity/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"task_id": "GEN2-021",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Самоизменение Домового не должно уметь уничтожить единственный механизм собственного отката.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-021-GOAL",
|
||||
"REQ-GEN2-021-BOUNDARY",
|
||||
"REQ-GEN2-021-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"runtime/supervisor/**",
|
||||
"cmd/supervisor/**",
|
||||
"deploy/**",
|
||||
"runtime/health/**",
|
||||
"scripts/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-021-add-independent-supervisor-lifeboat-for-release-health-and-rollback.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 1,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.commit@v1",
|
||||
"contracts.configuration@gen2"
|
||||
],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "Runtime only. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Supervisor writes runtime release state only",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: Supervisor writes runtime release state only"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass.",
|
||||
"Deploy/release surface changes are allowed only for the bounded purpose of this packet; production promotion requires the packet's stated evidence and rollback path.",
|
||||
"Supervisor/Lifeboat must remain operationally independent from the candidate runtime it watches."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: Supervisor itself versioned conservatively; previous release remains bootable",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M4; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-020."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-021-SCN-01: candidate release start",
|
||||
"GEN2-021-SCN-02: health/readiness contract",
|
||||
"GEN2-021-SCN-03: controlled traffic/switch",
|
||||
"GEN2-021-SCN-04: watchdog observation",
|
||||
"GEN2-021-SCN-05: automatic rollback to last known good",
|
||||
"GEN2-021-SCN-06: persist release/switch evidence outside candidate process dependency",
|
||||
"GEN2-021-SCN-07: emergency manual control path",
|
||||
"GEN2-021-SCN-08: не давать Supervisor доступ к semantic decisions пользователя",
|
||||
"GEN2-021-EXIT: сломанный candidate Assistant не блокирует восстановление последней рабочей версии."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...",
|
||||
"FAST: go vet ./runtime/supervisor/... ./runtime/health/... ./cmd/supervisor/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"task_id": "GEN2-022",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Замкнуть цикл weakness → proposal → candidate → evaluator → canary → promote/rollback без автономного обхода governance.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-022-GOAL",
|
||||
"REQ-GEN2-022-BOUNDARY",
|
||||
"REQ-GEN2-022-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"work",
|
||||
"self",
|
||||
"capability"
|
||||
],
|
||||
"paths": [
|
||||
"contracts/work/**",
|
||||
"modules/work/**",
|
||||
"adapters/developmentexecutor/**",
|
||||
"modules/self/**",
|
||||
"contracts/self/**",
|
||||
"modules/capability/**",
|
||||
"contracts/capability/**",
|
||||
"runtime/supervisor/**",
|
||||
"cmd/work-graph-runtime/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-022-wire-governed-self-evolution-loop-using-existing-development-gates.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.work@gen2",
|
||||
"contracts.self@gen2",
|
||||
"contracts.capability@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-022 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Work + SELF evidence + Supervisor promotion. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"existing governed-development writer remains; SELF proposes/evaluates, Supervisor handles release state",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: existing governed-development writer remains; SELF proposes/evaluates, Supervisor handles release state"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass.",
|
||||
"Self-evolution may propose/build/test candidates but promotion remains governed and independently evaluated."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: existing development gates remain; autonomous promotion feature switch off",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M4; test profile: MILESTONE_RECOVERY.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-021."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-022-SCN-01: SELF ImprovementNeed -> Work development candidate",
|
||||
"GEN2-022-SCN-02: expected improvement + must-not-worsen criteria",
|
||||
"GEN2-022-SCN-03: Creator != Evaluator enforced",
|
||||
"GEN2-022-SCN-04: historical replay/personal benchmark",
|
||||
"GEN2-022-SCN-05: sandbox/candidate build",
|
||||
"GEN2-022-SCN-06: shadow/canary where appropriate",
|
||||
"GEN2-022-SCN-07: Supervisor promotion/rollback",
|
||||
"GEN2-022-SCN-08: LearningLedger records accepted/rejected change and actual outcome",
|
||||
"GEN2-022-SCN-09: уровни change: knowledge, behavior, capability, core — с возрастающей строгостью",
|
||||
"GEN2-022-EXIT: Домовой способен доказуемо улучшить компонент и объяснить историю изменения, но не тайно переписать core."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/work/... ./modules/self/... ./modules/capability/... ./adapters/developmentexecutor/... ./runtime/supervisor/...",
|
||||
"FAST: go vet ./modules/work/... ./modules/self/... ./modules/capability/... ./adapters/developmentexecutor/... ./runtime/supervisor/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"MILESTONE M4: go test ./modules/work/... ./modules/commit/... ./modules/epistemics/... ./modules/continuity/... ./runtime/supervisor/... ./cmd/work-graph-runtime/... ./cmd/research-runtime/...",
|
||||
"MILESTONE M4 recovery: run focused restart/checkpoint/unknown-outcome/rollback drill; no automatic go test ./... if M3 FULL remains fresh and no global schema cutover occurred."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"task_id": "GEN2-023",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Пользователь говорит, чего хочет; Домовой меняет scoped configuration/starts work through normal semantics instead of forcing admin panels.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-023-GOAL",
|
||||
"REQ-GEN2-023-BOUNDARY",
|
||||
"REQ-GEN2-023-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction",
|
||||
"configuration",
|
||||
"work",
|
||||
"authority"
|
||||
],
|
||||
"paths": [
|
||||
"modules/interaction/**",
|
||||
"contracts/interaction/**",
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/creator-web/**",
|
||||
"modules/configuration/**",
|
||||
"contracts/configuration/**",
|
||||
"modules/work/**",
|
||||
"contracts/work/**",
|
||||
"contracts/authority/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-023-make-conversational-control-the-primary-configuration-and-operational-ux.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.interaction@v1",
|
||||
"contracts.configuration@gen2",
|
||||
"contracts.work@gen2",
|
||||
"contracts.authority@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-023 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Interaction + Configuration/Work contracts. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Configuration remains setting writer; Interaction writes user instruction/intent only",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Configuration remains setting writer; Interaction writes user instruction/intent only"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: admin/read APIs remain available; conversational mutations can be disabled",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M5; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-022."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-023-SCN-01: intent patterns for setting/querying configuration",
|
||||
"GEN2-023-SCN-02: explain effective scope/value in natural language",
|
||||
"GEN2-023-SCN-03: one-time override vs durable preference distinction",
|
||||
"GEN2-023-SCN-04: conversation creates SettingAssignment only through Configuration contract",
|
||||
"GEN2-023-SCN-05: dangerous config/core changes route through Consequence + Authority/Commit",
|
||||
"GEN2-023-SCN-06: Creator Web admin screens остаются diagnostics/emergency, не primary UX",
|
||||
"GEN2-023-EXIT: обычные настройки не требуют ручного редактирования ENV/admin UI."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/interaction/... ./modules/configuration/... ./modules/work/... ./cmd/core-runtime/... ./cmd/creator-web/...",
|
||||
"FAST: go vet ./modules/interaction/... ./modules/configuration/... ./modules/work/... ./cmd/core-runtime/... ./cmd/creator-web/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"task_id": "GEN2-024",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Не смешивать владельца, членов семьи, гостей, голосовые источники и shared/private memory.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-024-GOAL",
|
||||
"REQ-GEN2-024-BOUNDARY",
|
||||
"REQ-GEN2-024-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"identity",
|
||||
"world",
|
||||
"epistemics",
|
||||
"interaction"
|
||||
],
|
||||
"paths": [
|
||||
"modules/identity/**",
|
||||
"contracts/identity/**",
|
||||
"modules/world/**",
|
||||
"contracts/world/**",
|
||||
"modules/epistemics/**",
|
||||
"contracts/epistemics/**",
|
||||
"modules/interaction/**",
|
||||
"contracts/interaction/**",
|
||||
"adapters/postgres/identity_repository.go",
|
||||
"adapters/postgres/world_repository.go",
|
||||
"adapters/postgres/epistemics_repository.go",
|
||||
"adapters/postgres/interaction_repository.go",
|
||||
"database/migrations/*.sql",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-024-multi-person-realms-identity-boundaries-and-contextual-source-trust.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.identity@v1",
|
||||
"contracts.world@gen2",
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.interaction@v1"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-024 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Identity + World + Epistemics, separate responsibilities. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Identity authenticates principals; World resolves referents; Epistemics evaluates source reliability",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Identity authenticates principals; World resolves referents; Epistemics evaluates source reliability"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: single-owner mode remains possible; new realm metadata additive",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M5; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-023."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-024-SCN-01: PrincipalDataRealm/owner/shared scopes",
|
||||
"GEN2-024-SCN-02: utterance/source attribution with confidence",
|
||||
"GEN2-024-SCN-03: World person referents do not equal authenticated Principal automatically",
|
||||
"GEN2-024-SCN-04: SourceReliabilityAssessment = source × domain × freshness × history",
|
||||
"GEN2-024-SCN-05: identity ambiguity remains explicit",
|
||||
"GEN2-024-SCN-06: future speaker-recognition result enters as Observation, not authentication truth",
|
||||
"GEN2-024-SCN-07: Context Compiler respects realm/scope isolation",
|
||||
"GEN2-024-EXIT: Домовой может знать семейный контекст, не превращая услышанное имя/голос в полномочия владельца."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/identity/... ./modules/world/... ./modules/epistemics/... ./modules/interaction/... ./adapters/postgres",
|
||||
"FAST: go vet ./modules/identity/... ./modules/world/... ./modules/epistemics/... ./modules/interaction/... ./adapters/postgres",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"task_id": "GEN2-025",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Закрепить единый путь для text/audio/image/video/device observations, чтобы будущие микрофоны/камеры не потребовали нового ядра.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-025-GOAL",
|
||||
"REQ-GEN2-025-BOUNDARY",
|
||||
"REQ-GEN2-025-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction",
|
||||
"capability",
|
||||
"artifact",
|
||||
"world"
|
||||
],
|
||||
"paths": [
|
||||
"modules/interaction/**",
|
||||
"contracts/interaction/**",
|
||||
"modules/capability/**",
|
||||
"contracts/capability/**",
|
||||
"modules/artifact/**",
|
||||
"contracts/artifact/**",
|
||||
"modules/world/**",
|
||||
"contracts/world/**",
|
||||
"adapters/multimodal/**",
|
||||
"cmd/creator-web/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-025-multimodal-and-ambient-ingress-readiness-without-implementing-the-whole-.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.interaction@v1",
|
||||
"contracts.capability@gen2",
|
||||
"contracts.artifact@v1",
|
||||
"contracts.world@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-025 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Interaction ingress + Artifact + Capability + World refs. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Interaction/Artifact/Epistemics existing responsibilities",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: Interaction/Artifact/Epistemics existing responsibilities"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: existing chat/file routes remain compatible",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M5; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-024."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-025-SCN-01: normalise ingress envelope: modality/source/device/location/principal attribution/evidence refs",
|
||||
"GEN2-025-SCN-02: raw large media -> Artifact, derived transcript/vision result -> Observation",
|
||||
"GEN2-025-SCN-03: device/location are World refs",
|
||||
"GEN2-025-SCN-04: speech/OCR/vision results include provenance/model/version/confidence",
|
||||
"GEN2-025-SCN-05: future ambient streams can feed bounded observations/episodes without bypassing Interaction/Epistemics",
|
||||
"GEN2-025-SCN-06: output device choice остаётся будущим capability, не hardcoded in core",
|
||||
"GEN2-025-EXIT: будущая команда «покажи камеру улицы на телевизоре здесь» вписывается через World/Focus/Capability/Authority без смены core model."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./modules/interaction/... ./modules/capability/... ./modules/artifact/... ./modules/world/... ./adapters/multimodal/... ./cmd/creator-web/...",
|
||||
"FAST: go vet ./modules/interaction/... ./modules/capability/... ./modules/artifact/... ./modules/world/... ./adapters/multimodal/... ./cmd/creator-web/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"task_id": "GEN2-026",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Перевести реальные бытовые сценарии на Gen2 foundation и доказать, что технический протокол не просачивается пользователю.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-026-GOAL",
|
||||
"REQ-GEN2-026-BOUNDARY",
|
||||
"REQ-GEN2-026-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"interaction",
|
||||
"situation",
|
||||
"capability",
|
||||
"epistemics",
|
||||
"work",
|
||||
"continuity",
|
||||
"configuration",
|
||||
"self",
|
||||
"world"
|
||||
],
|
||||
"paths": [
|
||||
"cmd/core-runtime/**",
|
||||
"cmd/cognitive-worker/**",
|
||||
"cmd/creator-web/**",
|
||||
"modules/interaction/**",
|
||||
"modules/situation/**",
|
||||
"modules/capability/**",
|
||||
"modules/epistemics/**",
|
||||
"modules/work/**",
|
||||
"modules/continuity/**",
|
||||
"modules/configuration/**",
|
||||
"modules/self/**",
|
||||
"modules/world/**",
|
||||
"runtime/retrieval/**",
|
||||
"adapters/multimodal/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-026-migrate-household-behavior-to-gen2-context-memory-skills-with-local-degr.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.interaction@gen2",
|
||||
"contracts.situation@gen2",
|
||||
"contracts.capability@gen2",
|
||||
"contracts.epistemics@gen2",
|
||||
"contracts.work@gen2",
|
||||
"contracts.continuity@v1",
|
||||
"contracts.configuration@gen2",
|
||||
"contracts.self@gen2",
|
||||
"contracts.world@gen2",
|
||||
"contracts.cognitive@gen2"
|
||||
],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "Existing owners, integration task. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"no new canonical owner; integration switches reads/routes to Gen2",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: no new canonical owner; integration switches reads/routes to Gen2"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: per-subsystem switches to V1 compatibility remain until M6",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M5; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-025."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-026-SCN-01: перенести SCN-HH-001..010 на Gen2 paths",
|
||||
"GEN2-026-SCN-02: name/preference continuity through scoped retrieval",
|
||||
"GEN2-026-SCN-03: current data uses freshness/revalidation",
|
||||
"GEN2-026-SCN-04: travel/places follows Focus/location and source evidence",
|
||||
"GEN2-026-SCN-05: multi-action uses Work",
|
||||
"GEN2-026-SCN-06: reminder uses Continuity",
|
||||
"GEN2-026-SCN-07: multimodal uses capability/degraded path",
|
||||
"GEN2-026-SCN-08: creative request avoids unnecessary web",
|
||||
"GEN2-026-SCN-09: partial/unavailable/contradictory results produce honest answer",
|
||||
"GEN2-026-SCN-10: short natural response",
|
||||
"GEN2-026-EXIT: обычный пользователь не замечает internal Gen2 protocols; baseline household behavior preserved/improved."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/core-runtime/... ./cmd/cognitive-worker/... ./cmd/creator-web/... ./modules/interaction/... ./modules/situation/... ./modules/capability/... ./modules/epistemics/... ./modules/work/... ./modules/continuity/... ./runtime/retrieval/...",
|
||||
"FAST: go vet ./cmd/core-runtime/... ./cmd/cognitive-worker/... ./cmd/creator-web/... ./modules/interaction/... ./modules/situation/... ./modules/capability/... ./modules/epistemics/... ./modules/work/... ./modules/continuity/... ./runtime/retrieval/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"task_id": "GEN2-027",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Перевести production на Gen2 по контролируемому canary, сохранив мгновенный rollback к последнему good release.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-027-GOAL",
|
||||
"REQ-GEN2-027-BOUNDARY",
|
||||
"REQ-GEN2-027-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [
|
||||
"configuration"
|
||||
],
|
||||
"paths": [
|
||||
"deploy/**",
|
||||
"runtime/supervisor/**",
|
||||
"runtime/health/**",
|
||||
"modules/configuration/**",
|
||||
"contracts/configuration/**",
|
||||
"docs/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-027-production-canary-and-controlled-gen2-traffic-transition.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 1,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [
|
||||
"contracts.configuration@gen2"
|
||||
],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-027 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Operations/Supervisor. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"runtime release state switches via Supervisor; semantic writers already cut over by prior tasks",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"current state -> additive/shadow Gen2 path -> evidence comparison -> controlled cutover -> compatibility retention; writer rule: runtime release state switches via Supervisor; semantic writers already cut over by prior tasks"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass.",
|
||||
"Deploy/release surface changes are allowed only for the bounded purpose of this packet; production promotion requires the packet's stated evidence and rollback path."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: one-command/automatic revert to last-known-good release, schema remains backward compatible",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M5; test profile: FULL.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-026."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-027-SCN-01: build candidate from accepted milestone commit",
|
||||
"GEN2-027-SCN-02: deploy alongside or controlled replacement according to existing platform constraints",
|
||||
"GEN2-027-SCN-03: health + DB migration compatibility",
|
||||
"GEN2-027-SCN-04: bounded traffic/canary",
|
||||
"GEN2-027-SCN-05: compare errors/latency/unknown/recovery",
|
||||
"GEN2-027-SCN-06: household smoke",
|
||||
"GEN2-027-SCN-07: rollback drill",
|
||||
"GEN2-027-SCN-08: promote only после evidence",
|
||||
"GEN2-027-SCN-09: записать production composition snapshot в SELF/Learning Ledger where appropriate",
|
||||
"GEN2-027-EXIT: Gen2 handles production household traffic and rollback proven."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./runtime/supervisor/... ./runtime/health/... ./modules/configuration/...",
|
||||
"FAST: go vet ./runtime/supervisor/... ./runtime/health/... ./modules/configuration/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"FULL: go test ./...",
|
||||
"FULL: go vet ./...",
|
||||
"FULL M5: household + multimodal + degraded-mode + restart/recovery + production canary/rollback smoke on docker.cin.su."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"task_id": "GEN2-028",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Доказать, что legacy paths больше не являются активными writers/read dependencies.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-028-GOAL",
|
||||
"REQ-GEN2-028-BOUNDARY",
|
||||
"REQ-GEN2-028-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"cmd/**",
|
||||
"adapters/**",
|
||||
"runtime/**",
|
||||
"platform/**",
|
||||
"docs/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-028-inventory-v1-compatibility-usage-and-prove-retirement-readiness.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-028 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Migration/Governance. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"no change",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: no change"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: not applicable; this task is proof only",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M6; test profile: FAST.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-027."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-028-SCN-01: caller inventory",
|
||||
"GEN2-028-SCN-02: writer inventory",
|
||||
"GEN2-028-SCN-03: feature-switch telemetry",
|
||||
"GEN2-028-SCN-04: PersonalMemory fallback usage",
|
||||
"GEN2-028-SCN-05: ActiveProjectFocus compatibility usage",
|
||||
"GEN2-028-SCN-06: truth_bundles read usage",
|
||||
"GEN2-028-SCN-07: ordinary ENV fallback usage",
|
||||
"GEN2-028-SCN-08: V1 Skill view usage",
|
||||
"GEN2-028-SCN-09: rollback observation window evidence",
|
||||
"GEN2-028-SCN-10: сформировать explicit retirement list, каждый пункт с rollback consequence",
|
||||
"GEN2-028-EXIT: для каждого legacy path есть доказательство ACTIVE / SAFE_TO_RETIRE / KEEP_LONGER."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/... ./modules/... ./runtime/...",
|
||||
"FAST: go vet ./cmd/... ./modules/... ./runtime/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --"
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"task_id": "GEN2-029",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Удалить архитектурные костыли V1, которые мешают дальнейшему развитию, но сохранить raw/canonical historical evidence.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-029-GOAL",
|
||||
"REQ-GEN2-029-BOUNDARY",
|
||||
"REQ-GEN2-029-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"cmd/**",
|
||||
"adapters/**",
|
||||
"runtime/**",
|
||||
"platform/**",
|
||||
"database/migrations/*.sql",
|
||||
"docs/**",
|
||||
"tests/**",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-029-retire-proven-v1-paths-without-deleting-evidence.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"architecture/**",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 1
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-029 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "respective owners. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"Gen2 writers already active; V1 writers permanently disabled/removed",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: Gen2 writers already active; V1 writers permanently disabled/removed"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"Database changes must be additive next-numbered migrations only until an explicitly approved retirement task; never edit historical V1 migrations 001-045.",
|
||||
"At most one canonical writer is active for each semantic state; backfill/shadow reads do not create a second canonical writer.",
|
||||
"The positive new_database_owner_roles delta in this packet is a devctl-v1 database-surface compatibility gate; it does not authorize cross-owner SQL/FK/JOIN or an extra owner unless explicitly stated by this task."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: code rollback possible to pre-retirement tag only if schema remains readable; destructive DB deletion requires explicit backup/export and approval",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth.",
|
||||
"Cutover must keep the previous read/compatibility path available until the task or milestone explicitly proves retirement."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M6; test profile: FULL_RETIREMENT.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-028."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-029-SCN-01: retire PersonalMemory ContextText read path",
|
||||
"GEN2-029-SCN-02: retire project-only Focus source of truth",
|
||||
"GEN2-029-SCN-03: retire truth_bundles as active canonical path while retaining historical compatibility/export as needed",
|
||||
"GEN2-029-SCN-04: remove non-bootstrap ordinary ENV reads already migrated",
|
||||
"GEN2-029-SCN-05: retire implicit reflex substitutes superseded by explicit Reflex",
|
||||
"GEN2-029-SCN-06: remove dead feature switches only after observation",
|
||||
"GEN2-029-SCN-07: не удалять human utterances, observations, experiences, commits, outcomes, audit lineage",
|
||||
"GEN2-029-EXIT: нет двух архитектурных поколений в active runtime; historical evidence доступно."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/... ./modules/... ./runtime/...",
|
||||
"FAST: go vet ./cmd/... ./modules/... ./runtime/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"FULL: go test ./...",
|
||||
"FULL: go vet ./...",
|
||||
"FULL retirement gate: complete repository + recovery + migration/export verification BEFORE any irreversible V1 schema/data retirement."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"task_id": "GEN2-030",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Зафиксировать Gen2 как новый рабочий baseline и отделить фундамент от будущих capabilities.",
|
||||
"requirement_ids": [
|
||||
"REQ-GEN2-030-GOAL",
|
||||
"REQ-GEN2-030-BOUNDARY",
|
||||
"REQ-GEN2-030-GATE"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"docs/**",
|
||||
"architecture/**",
|
||||
"development/**",
|
||||
"dev/**",
|
||||
"modules/**",
|
||||
"contracts/**",
|
||||
"cmd/**",
|
||||
"adapters/**",
|
||||
"runtime/**",
|
||||
"platform/**",
|
||||
"tests/**"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": [
|
||||
"security/**",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
".gitea/workflows/**",
|
||||
"deploy/**",
|
||||
"database/**",
|
||||
"adapters/models/**"
|
||||
]
|
||||
},
|
||||
"allowed_architecture_delta": {
|
||||
"new_cross_module_dependencies": 0,
|
||||
"new_canonical_owners": 0,
|
||||
"new_privileges": 0,
|
||||
"new_third_party_dependencies": 0,
|
||||
"new_database_owner_roles": 0
|
||||
},
|
||||
"contracts": {
|
||||
"consume": [],
|
||||
"may_change": [
|
||||
"Only contracts explicitly within GEN2-030 allowed_change_scope; preserve Gen2 owner boundaries and backward compatibility unless this packet says cutover."
|
||||
]
|
||||
},
|
||||
"semantic_owner": "Architecture/Governance + all owners evidence. Follow GEN2 target architecture: owner boundaries are authoritative; Memory remains a read/retrieval fabric, World is not truth owner, SELF is not a God orchestrator.",
|
||||
"canonical_state": [
|
||||
"no semantic cutover",
|
||||
"No task may create a second canonical owner for an already-owned mutable semantic concept."
|
||||
],
|
||||
"state_transitions": [
|
||||
"task input -> bounded Gen2 change -> FAST verification -> merge to gen2-integration; canonical writer rule: no semantic cutover"
|
||||
],
|
||||
"side_effects_and_privileges": [
|
||||
"No external mutation may bypass existing Authority -> Commit -> Executor boundaries.",
|
||||
"Do not weaken architecture/security/test guardrails to make the task pass."
|
||||
],
|
||||
"persistence_and_migrations": [
|
||||
"No database schema change is authorized by this packet unless a revised task explicitly grants database/migration scope."
|
||||
],
|
||||
"failure_retry_restart": [
|
||||
"Rollback rule: previous M5 production tag remains last-known-good until Gen2 acceptance signed",
|
||||
"Retries/restarts must not create duplicate external side effects or a second canonical writer.",
|
||||
"Durable state required by this task must survive process restart; process-local memory is never the only source of truth."
|
||||
],
|
||||
"assumptions": [
|
||||
"Program milestone: M6; test profile: FULL_FINAL.",
|
||||
"Task branch starts from local gen2-integration; base_repo_revision=gen2-integration must resolve to the untouched integration base while the task branch is active.",
|
||||
"Codex reads DOMOVOY_CONSTITUTION.md, GEN2_TARGET_ARCHITECTURE.md, GEN2_CANONICAL_DATA_MODEL.md, GEN2_MIGRATION_MAP.md, GEN2_MASTER_PLAN.md and GEN2_EXECUTION_CONTRACT.md before non-trivial edits.",
|
||||
"Do not expand scope silently. If a required change falls outside allowed paths or changes ownership, return a typed gap and request a task revision.",
|
||||
"Depends on: GEN2-029."
|
||||
],
|
||||
"acceptance_scenarios": [
|
||||
"GEN2-030-SCN-01: FULL acceptance gate once",
|
||||
"GEN2-030-SCN-02: production smoke/restart/rollback confirmation",
|
||||
"GEN2-030-SCN-03: architecture registry reflects actual code",
|
||||
"GEN2-030-SCN-04: canonical data model vs actual schema reconciliation",
|
||||
"GEN2-030-SCN-05: Personal Benchmark baseline",
|
||||
"GEN2-030-SCN-06: Learning Ledger initial history",
|
||||
"GEN2-030-SCN-07: known gaps/future capabilities queue",
|
||||
"GEN2-030-SCN-08: новый stable tag `domovoy-gen2-baseline-*`",
|
||||
"GEN2-030-SCN-09: обновить PROJECT_STATUS/active task",
|
||||
"GEN2-030-EXIT: Gen2 is documented, production-proven, rollbackable and ready for capability growth/self-learning iterations."
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: go test ./cmd/... ./modules/... ./runtime/...",
|
||||
"FAST: go vet ./cmd/... ./modules/... ./runtime/...",
|
||||
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
|
||||
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
|
||||
"FAST independent: go run ./cmd/devctl/independent",
|
||||
"FAST whitespace: git diff --check gen2-integration --",
|
||||
"FULL: go test ./...",
|
||||
"FULL: go vet ./...",
|
||||
"FULL final: complete Gen2 acceptance, household scenarios, recovery, architecture, migration compatibility and production smoke."
|
||||
],
|
||||
"terminal_gap_states": [
|
||||
"BLOCKED_BY_SPEC_GAP",
|
||||
"BLOCKED_BY_ARCHITECTURE_GAP",
|
||||
"BLOCKED_BY_MISSING_CAPABILITY",
|
||||
"BLOCKED_BY_CONFLICT",
|
||||
"REQUIRES_MIGRATION_DECISION"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
# DOMOVOY Gen2 — Execution Contract for Codex
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Этот документ определяет, **как выполнять** `PRE-GEN2-000`, `ARCH-GEN2-000` и `GEN2-001…GEN2-030`.
|
||||
|
||||
Он не заменяет:
|
||||
- `DOMOVOY_CONSTITUTION.md`;
|
||||
- `GEN2_TARGET_ARCHITECTURE.md`;
|
||||
- `GEN2_CANONICAL_DATA_MODEL.md`;
|
||||
- `GEN2_MIGRATION_MAP.md`;
|
||||
- `GEN2_MASTER_PLAN.md`.
|
||||
|
||||
При конфликте действуют существующий `AGENTS.md` и canonical architecture authority order. Если конфликт нельзя снять однозначно — `BLOCKED_BY_SPEC_GAP` / `BLOCKED_BY_ARCHITECTURE_GAP`, а не обход.
|
||||
|
||||
## 2. Почему `ARCH-GEN2-000`, а не `GEN2-000`
|
||||
|
||||
Текущий `devctl` принимает `architecture_revision` только как:
|
||||
- принятый `ADR-*`; или
|
||||
- существующий architecture task с префиксом `ARCH-*`.
|
||||
|
||||
Поэтому Master Plan task `GEN2-000 — Adopt Gen2 ...` исполняется как:
|
||||
|
||||
`ARCH-GEN2-000`
|
||||
|
||||
Смысл задачи не изменён. Все последующие `GEN2-*` используют:
|
||||
|
||||
`"architecture_revision": "ARCH-GEN2-000"`
|
||||
|
||||
## 3. Нулевой принцип
|
||||
|
||||
**Цель — построить Gen2, а не бесконечно проверять тесты.**
|
||||
|
||||
Тесты дают достаточное доказательство сохранения критических свойств. Они не являются самостоятельным продуктом.
|
||||
|
||||
Не добавлять тест только потому, что можно добавить ещё один edge case.
|
||||
|
||||
## 4. PRE-GEN2: сначала сохранить всё текущее
|
||||
|
||||
До любых Gen2 semantic/schema изменений выполнить `PRE-GEN2-000`.
|
||||
|
||||
### 4.1 Инвентаризация
|
||||
|
||||
Проверить:
|
||||
- `git status`;
|
||||
- все worktrees;
|
||||
- все локальные ветки;
|
||||
- ветки, опережающие `main`;
|
||||
- staged / unstaged / untracked files;
|
||||
- незапушенные commits;
|
||||
- merge/rebase/cherry-pick state.
|
||||
|
||||
Ничего не удалять и не делать `reset --hard` до классификации каждого изменения.
|
||||
|
||||
### 4.2 Preservation
|
||||
|
||||
Для каждого актуального открытого изменения:
|
||||
1. понять происхождение;
|
||||
2. исключить secrets/generated/runtime garbage;
|
||||
3. сохранить актуальную работу commit-ом;
|
||||
4. не переписывать опубликованную историю;
|
||||
5. не терять конфликтующую ветку ради удобного merge.
|
||||
|
||||
### 4.3 Merge
|
||||
|
||||
После preservation:
|
||||
1. обновить локальный `main`;
|
||||
2. контролируемо слить всю актуальную работу;
|
||||
3. разрешить конфликты по смыслу, сохраняя существующее поведение;
|
||||
4. выполнить только PRE FAST gate;
|
||||
5. push `main`.
|
||||
|
||||
### 4.4 Baseline
|
||||
|
||||
После успешного push:
|
||||
- записать SHA как `GEN2_BASE_COMMIT`;
|
||||
- поставить baseline tag `domovoy-pre-gen2`;
|
||||
- сохранить baseline report;
|
||||
- создать ветку `gen2-integration` от baseline;
|
||||
- push `gen2-integration`.
|
||||
|
||||
Если tag уже существует, не перезаписывать его молча: использовать duplicate-safe имя с датой/коротким SHA и записать фактическое имя в report.
|
||||
|
||||
## 5. Как установить execution package
|
||||
|
||||
До `PRE-GEN2-000` в репозиторий копируются **только**:
|
||||
- `bootstrap/dev/tasks/PRE-GEN2-000-*.json` -> `dev/tasks/`;
|
||||
- `bootstrap/dev/ACTIVE_TASK.json` -> `dev/ACTIVE_TASK.json`.
|
||||
|
||||
Остальные Gen2 документы/task packets не должны случайно попасть в V1 baseline как «реализация Gen2».
|
||||
|
||||
После PRE baseline:
|
||||
1. checkout `gen2-integration`;
|
||||
2. создать branch для `ARCH-GEN2-000`;
|
||||
3. скопировать содержимое `program/docs/gen2/` в согласованные canonical repo paths;
|
||||
4. скопировать `program/dev/tasks/*.json` в `dev/tasks/`;
|
||||
5. заменить `dev/ACTIVE_TASK.json` содержимым `program/dev/ACTIVE_TASK.arch-gen2-000.json`;
|
||||
6. выполнить `ARCH-GEN2-000`.
|
||||
|
||||
## 6. Branch model
|
||||
|
||||
После PRE каждая задача выполняется отдельно.
|
||||
|
||||
### 6.1 Перед task
|
||||
|
||||
```text
|
||||
checkout gen2-integration
|
||||
pull/verify expected head
|
||||
create branch gen2/<task-id>-<short-slug>
|
||||
set dev/ACTIVE_TASK.json -> selected packet
|
||||
run preflight
|
||||
```
|
||||
|
||||
Все post-PRE task packets имеют:
|
||||
|
||||
`"base_repo_revision": "gen2-integration"`
|
||||
|
||||
Это намеренно.
|
||||
|
||||
Пока Codex работает в task branch, локальная ветка `gen2-integration` остаётся на исходной точке задачи. Поэтому `check-diff` видит **только diff текущего packet-а**, а не всю программу Gen2.
|
||||
|
||||
### 6.2 После FAST gate
|
||||
|
||||
1. `check-diff`;
|
||||
2. `git diff --check`;
|
||||
3. commit task branch;
|
||||
4. merge task branch -> `gen2-integration`;
|
||||
5. push `gen2-integration`;
|
||||
6. удалить task branch только после проверки merge.
|
||||
|
||||
Не сливать незавершённый packet в integration ради «сохранения прогресса»: для этого используются commits в task branch.
|
||||
|
||||
## 7. Milestone merge в main
|
||||
|
||||
`main` обновляется по реперным точкам, а не после каждой мелкой задачи.
|
||||
|
||||
Milestone exit:
|
||||
1. выполнить соответствующий `MILESTONE` или `FULL` gate на `gen2-integration`;
|
||||
2. только если gate зелёный — merge `gen2-integration` -> `main`;
|
||||
3. push `main`;
|
||||
4. поставить milestone tag;
|
||||
5. fast-forward `gen2-integration` до нового `main` merge commit;
|
||||
6. push `gen2-integration`.
|
||||
|
||||
Рекомендуемые tags:
|
||||
- `domovoy-gen2-m0`
|
||||
- `domovoy-gen2-m1`
|
||||
- …
|
||||
- `domovoy-gen2-m6`
|
||||
|
||||
Не перезаписывать существующий tag.
|
||||
|
||||
## 8. Test profiles
|
||||
|
||||
### 8.1 FAST — основной цикл
|
||||
|
||||
FAST выполняется на каждом task.
|
||||
|
||||
Состав:
|
||||
- targeted `go test` только изменённого owner/runtime;
|
||||
- targeted `go vet`;
|
||||
- `devctl preflight`;
|
||||
- `devctl check-diff`;
|
||||
- `devctl/independent`;
|
||||
- `git diff --check`;
|
||||
- один-два regression checks для фактического риска task-а.
|
||||
|
||||
**FAST не включает `go test ./...` по умолчанию.**
|
||||
|
||||
Если targeted command невозможно корректно сформировать из-за package topology, сначала объяснить почему; не переходить автоматически на полный suite без причины.
|
||||
|
||||
### 8.2 MILESTONE
|
||||
|
||||
MILESTONE запускается после завершения целого архитектурного органа.
|
||||
|
||||
Только:
|
||||
- затронутые owners;
|
||||
- соседние contracts;
|
||||
- migration/backfill sample;
|
||||
- ключевые cross-owner regressions;
|
||||
- restart/recovery там, где это часть milestone.
|
||||
|
||||
### 8.3 FULL
|
||||
|
||||
FULL запускается только там, где packet прямо содержит `FULL:` evidence.
|
||||
|
||||
Базовые FULL points:
|
||||
- `GEN2-016` — M3;
|
||||
- `GEN2-027` — M5;
|
||||
- `GEN2-029` — до необратимого V1 retirement;
|
||||
- `GEN2-030` — финальный acceptance.
|
||||
|
||||
Дополнительный FULL допустим только при:
|
||||
- критической глобальной schema migration;
|
||||
- изменении Authority/Commit invariants;
|
||||
- серьёзном production rollback concern;
|
||||
- явном требовании владельца.
|
||||
|
||||
### 8.4 Не повторять свежий FULL без причины
|
||||
|
||||
Если предыдущий FULL:
|
||||
- выполнен на том же milestone;
|
||||
- после него не менялась соответствующая глобальная поверхность;
|
||||
- нет нового риска, который он должен покрыть,
|
||||
|
||||
не запускать его повторно «для уверенности».
|
||||
|
||||
## 9. STOP TEST EXPANSION
|
||||
|
||||
Codex обязан остановить расширение тестов и вернуться к цели task-а, если одновременно верно:
|
||||
|
||||
1. функциональная/архитектурная цель ещё не достигнута;
|
||||
2. тестовая работа стала существенно больше самой реализации;
|
||||
3. новые тесты проверяют уже защищённый invariant или косметические вариации;
|
||||
4. нет нового существенного риска, ради которого они нужны.
|
||||
|
||||
В этом случае:
|
||||
- оставить минимально достаточные tests;
|
||||
- записать потенциальные дополнительные cases в findings;
|
||||
- продолжить реализацию.
|
||||
|
||||
## 10. Architecture and migration law
|
||||
|
||||
### 10.1 Один canonical writer
|
||||
|
||||
Никогда не иметь два canonical writers одного mutable semantic state.
|
||||
|
||||
Разрешено:
|
||||
- backfill;
|
||||
- shadow projection;
|
||||
- dual-read;
|
||||
- compare;
|
||||
- read fallback.
|
||||
|
||||
Не разрешено:
|
||||
- два равноправных canonical write paths «на время миграции».
|
||||
|
||||
### 10.2 Historical migrations
|
||||
|
||||
Не редактировать V1 migrations `001–045`.
|
||||
|
||||
Gen2 создаёт только новые next-numbered migrations.
|
||||
|
||||
Если порядковый номер изменился из-за параллельной работы, использовать следующий свободный — смысл migration определяется task, а не заранее зарезервированным числом.
|
||||
|
||||
### 10.3 Особенность текущего devctl
|
||||
|
||||
Текущий validator трактует **любое** изменение `database/**` как требующее положительного `new_database_owner_roles`, а любое изменение `deploy/**` — положительного `new_privileges`.
|
||||
|
||||
Поэтому packets на этих поверхностях содержат `1` как compatibility gate.
|
||||
|
||||
Это **не означает**, что task автоматически разрешает:
|
||||
- новый semantic owner;
|
||||
- новый DB role;
|
||||
- cross-owner FK/JOIN/write;
|
||||
- дополнительную внешнюю привилегию.
|
||||
|
||||
Разрешено только то, что явно написано в packet + Gen2 architecture docs.
|
||||
|
||||
`ARCH-GEN2-000` может улучшить этот validator, но должен сохранить обратную совместимость с уже созданными packets.
|
||||
|
||||
## 11. Scope discipline
|
||||
|
||||
Task packet — верхняя граница изменения.
|
||||
|
||||
Если Codex обнаружил, что нужно изменить файл вне scope:
|
||||
1. не менять файл;
|
||||
2. установить, это implementation need или architecture gap;
|
||||
3. вернуть соответствующий `BLOCKED_*`, либо подготовить revision task packet;
|
||||
4. после явного расширения scope продолжить.
|
||||
|
||||
Не расширять task до соседнего рефакторинга только потому, что он «логично рядом».
|
||||
|
||||
## 12. Read before write
|
||||
|
||||
Перед нетривиальным изменением:
|
||||
1. прочитать Gen2 canonical docs;
|
||||
2. прочитать текущий task packet;
|
||||
3. найти существующую реализацию/owner;
|
||||
4. проверить migrations/contracts/call path;
|
||||
5. только затем менять код.
|
||||
|
||||
Если существующий механизм уже решает задачу — расширить его, а не создавать параллельный.
|
||||
|
||||
## 13. Data migration rules
|
||||
|
||||
Для каждого cutover:
|
||||
1. additive schema;
|
||||
2. backfill с lineage;
|
||||
3. shadow/read comparison;
|
||||
4. controlled reader switch;
|
||||
5. writer switch только после доказательства;
|
||||
6. observation window;
|
||||
7. V1 compatibility path остаётся до retirement task.
|
||||
|
||||
Rollback не должен уничтожать уже накопленные Gen2 evidence.
|
||||
|
||||
## 14. Production
|
||||
|
||||
Production mutation допускается только packet-ами, где это явно разрешено.
|
||||
|
||||
Основной production transition — `GEN2-027`.
|
||||
|
||||
Перед promotion:
|
||||
- health;
|
||||
- migration compatibility;
|
||||
- bounded canary;
|
||||
- household smoke;
|
||||
- rollback drill.
|
||||
|
||||
Если canary неубедителен — rollback считается нормальным результатом, а не провалом программы.
|
||||
|
||||
## 15. Self-evolution
|
||||
|
||||
До `GEN2-022` SELF/Skill/Reflex не получает права самовольно менять production code.
|
||||
|
||||
После `GEN2-022`:
|
||||
- proposal != promotion;
|
||||
- creator != evaluator;
|
||||
- candidate строится отдельно;
|
||||
- tests/shadow/canary;
|
||||
- Supervisor держит last-known-good;
|
||||
- owner intent/authority остаётся выше внутренней оптимизации.
|
||||
|
||||
## 16. Completion report каждого task
|
||||
|
||||
Codex обязан закончить packet коротким отчётом:
|
||||
|
||||
- task id/revision;
|
||||
- base SHA;
|
||||
- branch/commit SHA;
|
||||
- changed paths/modules;
|
||||
- contracts/schema/events/dependencies/privileges touched;
|
||||
- writer before/after;
|
||||
- migration/backfill/cutover state;
|
||||
- FAST/MILESTONE/FULL tests **фактически** выполненные;
|
||||
- tests сознательно не выполненные и почему;
|
||||
- rollback point;
|
||||
- findings not changed;
|
||||
- gaps/limitations;
|
||||
- следующий task, который разрешено активировать.
|
||||
|
||||
Не писать `PASS` для теста, который не запускался.
|
||||
|
||||
## 17. Terminal states
|
||||
|
||||
Допустимые остановки:
|
||||
- `BLOCKED_BY_SPEC_GAP`
|
||||
- `BLOCKED_BY_ARCHITECTURE_GAP`
|
||||
- `BLOCKED_BY_MISSING_CAPABILITY`
|
||||
- `BLOCKED_BY_CONFLICT`
|
||||
- `REQUIRES_MIGRATION_DECISION`
|
||||
|
||||
Typed stop лучше скрытого архитектурного обхода.
|
||||
|
||||
## 18. Главный критерий программы
|
||||
|
||||
Успех task-а — не «все возможные тесты зелёные».
|
||||
|
||||
Успех:
|
||||
|
||||
1. заявленная Gen2 capability/architecture increment реально создана;
|
||||
2. canonical ownership не размыт;
|
||||
3. предыдущий критический behavior не сломан;
|
||||
4. есть достаточное evidence;
|
||||
5. rollback понятен;
|
||||
6. следующий packet может строиться на результате без архитектурного долга, который мы сознательно откладываем.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user