From 4a662ec2d32e51b9a91ac8a0fd3363abf0b1f5e2 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 14:11:44 +0000 Subject: [PATCH 1/4] feat(events): gate shared-dev consumer mode changes Add ValidateOpsCutover, eventmode CLI, and a host script that pin agent_system_dev and refuse FAV plus test/main before any cluster mode write. Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 5 +- .../2026-09-15-event-shared-live-cutover.md | 50 ++++++ cmd/eventmode/main.go | 113 ++++++++++++ internal/events/cutover.go | 170 ++++++++++++++++++ internal/events/cutover_script_test.go | 44 +++++ internal/events/cutover_test.go | 34 ++++ internal/events/envelope.go | 1 + scripts/event-consumer-mode.sh | 59 ++++++ 8 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-15-event-shared-live-cutover.md create mode 100644 cmd/eventmode/main.go create mode 100644 internal/events/cutover.go create mode 100644 internal/events/cutover_script_test.go create mode 100644 internal/events/cutover_test.go create mode 100755 scripts/event-consumer-mode.sh diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 1d41fa2e..2e53c00f 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,7 +5,7 @@ ## 焦点 -1. **TRACK-E/S**: Phase 2 隔离库与结构解耦已合入;共享 live 见 [长期任务](./long-term/2026-09-15-event-shared-live-cutover.md) +1. **TRACK-E LIVE**: 共享 consumer 切换([short-term](./short-term/2026-09-15-event-shared-live-cutover.md)) 2. **TRACK-P**: 立项完成([H0](./research/2026-09-15-http-error-envelope-h0-inventory.md) / [P0](./research/2026-09-15-pi-typed-result-p0-matrix.md));实施见 [长期任务](./long-term/2026-09-15-track-p-protocol-implementation.md) 3. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针) @@ -70,6 +70,7 @@ | Cursor | bizdev-locator-removal | fix/bizdev-locator-removal | - | bizdev | done | | Cursor | chatflow-orchestration-boundary | fix/chatflow-orchestration-boundary | - | chat | done | | Cursor | phase2-closeout-inventories | feature/phase2-closeout-inventories | - | architecture-docs | done | +| Cursor | event-shared-live-cutover | feature/event-shared-live-cutover | - | events | in_progress | > 新任务开始前登记。`branch` 必须为 `feature/` 或 `fix/`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 @@ -81,7 +82,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| - | idle | - | - | - | +| Cursor | e2e | 2026-09-15 | shared event consumer live cutover;FAV off | - | **phase**: `idle` | `merging` | `smoke` | `e2e` diff --git a/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md b/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md new file mode 100644 index 00000000..4b5ee859 --- /dev/null +++ b/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md @@ -0,0 +1,50 @@ +--- +title: "共享环境事件消费者 live 切换" +status: in_progress +created: 2026-09-15 +updated: 2026-09-15 +owner: Cursor +branch: feature/event-shared-live-cutover +worktree: "-" +mutex: events +--- + +# 共享环境事件消费者 live 切换 + +## 目标 + +在共享 `agent-dev` 上按 notify → workflow_advance → projection 完成 shadow → drain → live → 回滚演练,再删除生产 hook 注册。 + +## 范围 + +- `scripts/event-consumer-mode.sh` + `cmd/eventmode` +- 共享 `agent_system_dev` inventory / quarantine +- 逐消费者 live 与授权页纵切(项目 6275;143 只读) + +## 非目标 + +- FAV live +- test/main 环境 +- 编辑 Phase 2 计划原文 +- Track P(独立波次) + +## 步骤 + +1. [x] L0 受控 SetMode 与 inventory +2. [ ] L1 notify +3. [ ] L2 workflow_advance +4. [ ] L3 projection + 授权页 +5. [ ] L4 删除生产 hook 注册 + +## 验收标准 + +- [ ] fav / test / main 切模式失败 +- [ ] 每消费者独立 live 报告 +- [ ] 生产代码无 `OnOutbox =` / `AddOnApprovedHook(` + +## 相关链接 + +- Long-term: [.doc/ops/long-term/2026-09-15-event-shared-live-cutover.md](../long-term/2026-09-15-event-shared-live-cutover.md) +- Test report: (本波完成后补) + +## 证据 / 备注 diff --git a/cmd/eventmode/main.go b/cmd/eventmode/main.go new file mode 100644 index 00000000..ef6590d7 --- /dev/null +++ b/cmd/eventmode/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + "chat-agent-system/internal/config" + "chat-agent-system/internal/events" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + envPath := os.Getenv("EVENTMODE_ENV") + if envPath == "" { + envPath = "configs/.env.dev" + } + cfg, err := config.Load(envPath) + if err != nil { + fmt.Fprintln(os.Stderr, "eventmode: load env") + os.Exit(1) + } + if cfg.Env != events.OpsCutoverEnvironment { + fmt.Fprintln(os.Stderr, "eventmode: env must be dev") + os.Exit(1) + } + if cfg.ContextDbName != "agent_system_dev" { + fmt.Fprintln(os.Stderr, "eventmode: database must be agent_system_dev") + os.Exit(1) + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + pool, err := pgxpool.New(ctx, cfg.ContextDSN()) + if err != nil { + fmt.Fprintln(os.Stderr, "eventmode: pool") + os.Exit(1) + } + defer pool.Close() + + switch os.Args[1] { + case "inventory": + if err := runInventory(ctx, pool); err != nil { + fmt.Fprintln(os.Stderr, "eventmode: inventory failed") + os.Exit(1) + } + case "set": + if len(os.Args) != 4 { + usage() + os.Exit(2) + } + consumer, mode := os.Args[2], os.Args[3] + before, _, _ := events.LoadMode(ctx, pool, cfg.Env, consumer) + if err := events.ApplyOpsCutover(ctx, pool, cfg.Env, consumer, mode); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + after, rev, _ := events.LoadMode(ctx, pool, cfg.Env, consumer) + enc := json.NewEncoder(os.Stdout) + _ = enc.Encode(map[string]any{ + "environment": cfg.Env, "consumer": consumer, + "before": before, "after": after, "revision": rev, + }) + case "quarantine-illegal": + n, err := events.QuarantineIllegalPending(ctx, pool, "cursor-ops", "phase2-shared-live") + if err != nil { + fmt.Fprintln(os.Stderr, "eventmode: quarantine failed") + os.Exit(1) + } + _ = json.NewEncoder(os.Stdout).Encode(map[string]any{"quarantined": n}) + default: + usage() + os.Exit(2) + } +} + +func runInventory(ctx context.Context, pool *pgxpool.Pool) error { + modes, err := events.ListModes(ctx, pool) + if err != nil { + return err + } + backlog, err := events.ListBacklog(ctx, pool) + if err != nil { + return err + } + pending, err := events.ClassifyPending(ctx, pool) + if err != nil { + return err + } + type wire struct { + EventID string `json:"event_id"` + Consumer string `json:"consumer"` + Classification string `json:"classification"` + Reason string `json:"reason"` + } + class := make([]wire, 0, len(pending)) + for _, r := range pending { + class = append(class, wire{r.EventID, r.Consumer, r.Classification, r.Reason}) + } + return json.NewEncoder(os.Stdout).Encode(map[string]any{ + "modes": modes, "backlog": backlog, "pending_classifications": class, + }) +} + +func usage() { + fmt.Fprintln(os.Stderr, "usage: eventmode inventory | set | quarantine-illegal") +} diff --git a/internal/events/cutover.go b/internal/events/cutover.go new file mode 100644 index 00000000..c5078b8a --- /dev/null +++ b/internal/events/cutover.go @@ -0,0 +1,170 @@ +package events + +import ( + "context" + "fmt" + "strings" + + "chat-agent-system/internal/app/uow" + + "github.com/jackc/pgx/v5/pgxpool" +) + +const OpsCutoverEnvironment = "dev" + +// ValidateOpsCutover is the shared-dev gate for cluster mode changes. +// test/main, FAV, and unknown consumers are rejected before any write. +func ValidateOpsCutover(environment, consumer, mode string) error { + if strings.TrimSpace(environment) != OpsCutoverEnvironment { + return fmt.Errorf("%w: environment %q", ErrOpsCutover, environment) + } + switch consumer { + case ConsumerNotify, ConsumerWorkflowAdvance, ConsumerProjection: + default: + return fmt.Errorf("%w: consumer %q", ErrOpsCutover, consumer) + } + switch mode { + case ModeOff, ModeShadow, ModeLive: + default: + return fmt.Errorf("%w: mode %q", ErrOpsCutover, mode) + } + return nil +} + +func ApplyOpsCutover(ctx context.Context, pool *pgxpool.Pool, environment, consumer, mode string) error { + if err := ValidateOpsCutover(environment, consumer, mode); err != nil { + return err + } + return SetMode(ctx, pool, environment, consumer, mode) +} + +type ModeSnapshot struct { + Environment string `json:"environment"` + Consumer string `json:"consumer"` + Mode string `json:"mode"` + Revision int64 `json:"revision"` +} + +type BacklogRow struct { + Consumer string `json:"consumer"` + Status string `json:"status"` + Count int64 `json:"count"` +} + +func ListModes(ctx context.Context, pool *pgxpool.Pool) ([]ModeSnapshot, error) { + rows, err := pool.Query(ctx, ` + SELECT environment, consumer, mode, revision + FROM event_consumer_modes + ORDER BY environment, consumer + `) + if err != nil { + return nil, err + } + defer rows.Close() + var out []ModeSnapshot + for rows.Next() { + var m ModeSnapshot + if err := rows.Scan(&m.Environment, &m.Consumer, &m.Mode, &m.Revision); err != nil { + return nil, err + } + out = append(out, m) + } + return out, rows.Err() +} + +func ListBacklog(ctx context.Context, pool *pgxpool.Pool) ([]BacklogRow, error) { + rows, err := pool.Query(ctx, ` + SELECT consumer, status, COUNT(*) + FROM event_deliveries + GROUP BY consumer, status + ORDER BY consumer, status + `) + if err != nil { + return nil, err + } + defer rows.Close() + var out []BacklogRow + for rows.Next() { + var r BacklogRow + if err := rows.Scan(&r.Consumer, &r.Status, &r.Count); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +type pendingDelivery struct { + EventID string + Consumer string + EventName string + Environment string + ProjectID int64 + SchemaVersion int + PayloadDigest string +} + +func loadPending(ctx context.Context, pool *pgxpool.Pool) ([]pendingDelivery, error) { + rows, err := pool.Query(ctx, ` + SELECT d.event_id, d.consumer, e.event_name, e.environment, e.project_id, e.schema_version, COALESCE(e.payload_digest, '') + FROM event_deliveries d + INNER JOIN event_envelopes e ON e.event_id = d.event_id + WHERE d.status IN ('pending','leased') + `) + if err != nil { + return nil, err + } + defer rows.Close() + var out []pendingDelivery + for rows.Next() { + var p pendingDelivery + if err := rows.Scan(&p.EventID, &p.Consumer, &p.EventName, &p.Environment, &p.ProjectID, &p.SchemaVersion, &p.PayloadDigest); err != nil { + return nil, err + } + out = append(out, p) + } + return out, rows.Err() +} + +func ClassifyPending(ctx context.Context, pool *pgxpool.Pool) ([]InventoryRow, error) { + pending, err := loadPending(ctx, pool) + if err != nil { + return nil, err + } + out := make([]InventoryRow, 0, len(pending)) + for _, p := range pending { + env := Envelope{ + EventID: p.EventID, EventName: p.EventName, SchemaVersion: p.SchemaVersion, + Environment: p.Environment, ProjectID: p.ProjectID, PayloadDigest: p.PayloadDigest, + } + out = append(out, ClassifyEnvelope(env, p.Consumer)) + } + return out, nil +} + +// QuarantineIllegalPending persists quarantine for pending deliveries whose +// consumer is not on the contract (including FAV). Safe envelopes are left untouched. +func QuarantineIllegalPending(ctx context.Context, pool *pgxpool.Pool, operator, approval string) (int, error) { + if operator == "" || approval == "" { + return 0, ErrIntegrityFailure + } + rows, err := ClassifyPending(ctx, pool) + if err != nil { + return 0, err + } + n := 0 + unit := uow.NewPG(pool) + for _, row := range rows { + if row.Classification != ClassificationQuarantine { + continue + } + err := unit.Within(ctx, func(scope uow.TxScope) error { + return PersistInventory(uow.WithScope(ctx, scope), row, operator, approval) + }) + if err != nil { + return n, err + } + n++ + } + return n, nil +} diff --git a/internal/events/cutover_script_test.go b/internal/events/cutover_script_test.go new file mode 100644 index 00000000..f928a524 --- /dev/null +++ b/internal/events/cutover_script_test.go @@ -0,0 +1,44 @@ +package events_test + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestEventConsumerModeScriptGates(t *testing.T) { + root := repoRoot(t) + text, err := os.ReadFile(filepath.Join(root, "scripts", "event-consumer-mode.sh")) + if err != nil { + t.Fatal(err) + } + s := string(text) + if !strings.Contains(s, "fav") || !strings.Contains(s, "refusing test/main") { + t.Fatal("script must reject fav and test/main") + } + if strings.Contains(s, "5434") || strings.Contains(s, "5435") { + t.Fatal("script must not pin test/main ports") + } + if !strings.Contains(s, "agent_system_dev") || !strings.Contains(s, "5433") { + t.Fatal("script must pin shared dev db") + } +} + +func repoRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("go.mod not found") + } + dir = parent + } +} diff --git a/internal/events/cutover_test.go b/internal/events/cutover_test.go new file mode 100644 index 00000000..36658e37 --- /dev/null +++ b/internal/events/cutover_test.go @@ -0,0 +1,34 @@ +package events + +import "testing" + +func TestValidateOpsCutoverRejectsFavAndNonDev(t *testing.T) { + if err := ValidateOpsCutover("dev", ConsumerNotify, ModeShadow); err != nil { + t.Fatal(err) + } + if err := ValidateOpsCutover("test", ConsumerNotify, ModeLive); err == nil { + t.Fatal("test env must fail") + } + if err := ValidateOpsCutover("main", ConsumerNotify, ModeLive); err == nil { + t.Fatal("main env must fail") + } + if err := ValidateOpsCutover("dev", ConsumerFAV, ModeOff); err == nil { + t.Fatal("fav must fail even at off") + } + if err := ValidateOpsCutover("dev", "unknown", ModeLive); err == nil { + t.Fatal("unknown consumer must fail") + } + if err := ValidateOpsCutover("dev", ConsumerNotify, "dual"); err == nil { + t.Fatal("unknown mode must fail") + } +} + +func TestValidateOpsCutoverAllowsThreeConsumers(t *testing.T) { + for _, c := range []string{ConsumerNotify, ConsumerWorkflowAdvance, ConsumerProjection} { + for _, m := range []string{ModeOff, ModeShadow, ModeLive} { + if err := ValidateOpsCutover("dev", c, m); err != nil { + t.Fatalf("%s %s: %v", c, m, err) + } + } + } +} diff --git a/internal/events/envelope.go b/internal/events/envelope.go index ddd7bfa4..039c62e3 100644 --- a/internal/events/envelope.go +++ b/internal/events/envelope.go @@ -23,6 +23,7 @@ var ( ErrDisabledConsumer = errors.New("events: consumer is disabled") ErrUnregistered = errors.New("events: event or consumer is not in the static registry") ErrReplayApply = errors.New("events: replay apply rejected") + ErrOpsCutover = errors.New("events: shared cutover rejected") ) type Envelope struct { diff --git a/scripts/event-consumer-mode.sh b/scripts/event-consumer-mode.sh new file mode 100755 index 00000000..006fd865 --- /dev/null +++ b/scripts/event-consumer-mode.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Shared agent-dev event consumer mode switch. +# Only environment=dev, consumers notify|workflow_advance|projection, modes off|shadow|live. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +ENV_FILE="${EVENTMODE_ENV:-configs/.env.dev}" + +usage() { + echo "usage: $0 inventory | set | quarantine-illegal" >&2 + exit 2 +} + +if [[ $# -lt 1 ]]; then + usage +fi + +if [[ ! -f "$ENV_FILE" ]]; then + echo "event-consumer-mode: missing $ENV_FILE" >&2 + exit 1 +fi + +if grep -E -q '^ENV=(test|main)[[:space:]]*$' "$ENV_FILE"; then + echo "event-consumer-mode: refusing test/main env file" >&2 + exit 1 +fi + +# Host-side ops pin the published dev database; never test/main ports. +export CONTEXT_DB_HOST="${CONTEXT_DB_HOST:-127.0.0.1}" +export CONTEXT_DB_PORT="${CONTEXT_DB_PORT:-5433}" +export CONTEXT_DB_NAME="${CONTEXT_DB_NAME:-agent_system_dev}" +export ENV=dev +export EVENTMODE_ENV="$ENV_FILE" + +case "$1" in + inventory) + go run ./cmd/eventmode inventory + ;; + set) + if [[ $# -ne 3 ]]; then + usage + fi + consumer="$2" + mode="$3" + if [[ "$consumer" == "fav" ]]; then + echo "event-consumer-mode: fav is disabled" >&2 + exit 1 + fi + go run ./cmd/eventmode set "$consumer" "$mode" + ;; + quarantine-illegal) + go run ./cmd/eventmode quarantine-illegal + ;; + *) + usage + ;; +esac From 9b1a94c5bcb3584c66ce1beafed76f4c45297527 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 14:23:07 +0000 Subject: [PATCH 2/4] feat(app): drop production outbox and approval hook registration Durable notify/advance/projection are live on shared dev, so composition roots must not assign OnOutbox or AddOnApprovedHook. Bind helpers remain for tests and a rollback window. Co-authored-by: Cursor --- internal/app/app.go | 12 ++---- internal/app/unhook_test.go | 48 +++++++++++++++++++++++ internal/server/workflow_approval_hook.go | 7 ++-- 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 internal/app/unhook_test.go diff --git a/internal/app/app.go b/internal/app/app.go index 6ee92ef7..27fe690c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -201,22 +201,16 @@ func (r *Runtime) bindDocumentNotify() { if r == nil || r.notifyBound { return } - if r.Biz != nil && r.Rev != nil { - if docnotify.Bind(r.Rev, r.DocNotify) { - r.subscribers++ - } - } + // Durable notify is live; do not assign OnOutbox in production. r.notifyBound = true } func (r *Runtime) bindWorkflowApprovalHook() { - if r == nil || r.hookBound || r.Gov == nil || r.Inst == nil || !r.Inst.Available() { - r.hookBound = true + if r == nil || r.hookBound { return } - r.Gov.AddOnApprovedHook(r.onWorkflowDocumentApproved) + // Durable workflow_advance is live; do not register AddOnApprovedHook in production. r.hookBound = true - r.subscribers++ } // WriterLive reports whether the durable consumer already owns the visible write. diff --git a/internal/app/unhook_test.go b/internal/app/unhook_test.go new file mode 100644 index 00000000..07479924 --- /dev/null +++ b/internal/app/unhook_test.go @@ -0,0 +1,48 @@ +package app + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestProductionCompositionHasNoHookRegistration(t *testing.T) { + root := findGoMod(t) + files := []string{ + filepath.Join(root, "internal", "app", "app.go"), + filepath.Join(root, "internal", "server", "workflow_approval_hook.go"), + filepath.Join(root, "internal", "server", "server.go"), + } + for _, f := range files { + b, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + src := string(b) + if strings.Contains(src, "OnOutbox =") { + t.Fatalf("%s still assigns OnOutbox", f) + } + if strings.Contains(src, "AddOnApprovedHook(") { + t.Fatalf("%s still registers AddOnApprovedHook", f) + } + } +} + +func findGoMod(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("go.mod not found") + } + dir = parent + } +} diff --git a/internal/server/workflow_approval_hook.go b/internal/server/workflow_approval_hook.go index abd85414..3a77ca5b 100644 --- a/internal/server/workflow_approval_hook.go +++ b/internal/server/workflow_approval_hook.go @@ -9,10 +9,9 @@ import ( ) func (s *Server) registerWorkflowApprovalHook() { - if s == nil || s.gov == nil || s.instSvc == nil || !s.instSvc.Available() { - return - } - s.gov.AddOnApprovedHook(s.onWorkflowDocumentApproved) + // Durable workflow_advance is the unique writer. Keep the method for one + // rollback window; do not register AddOnApprovedHook in production. + return } func (s *Server) onWorkflowDocumentApproved(ctx context.Context, doc *docgov.DocumentRecord) { From ad9027fe8fd7b2af11544af64890c6410a835772 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 14:23:07 +0000 Subject: [PATCH 3/4] docs(test): record shared event live cutover and close the reliability pilot Notify, workflow_advance, and projection completed shadow/live/rollback on agent-dev; authorized 6275 digest matches; project 143 stays non-leaking. Co-authored-by: Cursor --- .../2026-09-15-event-shared-live-cutover.md | 4 +- .doc/ops/CURRENT.md | 4 +- .../2026-09-15-event-shared-live-cutover.md | 2 +- .doc/remediation/gap-matrix.md | 2 +- .doc/status/FEATURE-MATRIX.md | 2 +- ...-event-e2-unhook-production-test-report.md | 47 ++++++++++++++ ...15-event-notify-shared-live-test-report.md | 61 ++++++++++++++++++ ...vent-projection-shared-live-test-report.md | 57 ++++++++++++++++ ...orkflow-advance-shared-live-test-report.md | 51 +++++++++++++++ .../__pycache__/dual-tab-live.cpython-314.pyc | Bin 0 -> 27082 bytes .../advance-live-again.json | 1 + .../advance-live.json | 1 + .../advance-rollback-off.json | 1 + .../advance-shadow.json | 1 + .../auth-6275-content-keys.json | 15 +++++ .../auth-6275-content-status.txt | 1 + .../auth-6275-revision-state.json | 18 ++++++ .../authz-143-negative.json | 5 ++ .../delivery-status-after-projection.txt | 6 ++ .../events-ready-shadow-notify.json | 1 + .../health-live-notify.json | 1 + .../health-shadow-notify.json | 1 + .../inventory-after-quarantine.json | 1 + .../modes-after-advance.txt | 8 +++ .../modes-after-notify.txt | 8 +++ .../modes-after-projection.txt | 8 +++ .../notify-live-again.json | 1 + .../notify-live.json | 1 + .../notify-rollback-off.json | 1 + .../notify-shadow.json | 1 + .../projection-live-again.json | 1 + .../projection-live.json | 1 + .../projection-rollback-off.json | 1 + .../projection-shadow.json | 1 + .../quarantine.json | 1 + .../unauth-6275.json | 4 ++ 36 files changed, 313 insertions(+), 7 deletions(-) rename .doc/{ops/long-term => archive/ops}/2026-09-15-event-shared-live-cutover.md (90%) create mode 100644 .doc/test-reports/2026-09-15-event-e2-unhook-production-test-report.md create mode 100644 .doc/test-reports/2026-09-15-event-notify-shared-live-test-report.md create mode 100644 .doc/test-reports/2026-09-15-event-projection-shared-live-test-report.md create mode 100644 .doc/test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md create mode 100644 .doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/__pycache__/dual-tab-live.cpython-314.pyc create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/advance-live-again.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/advance-live.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/advance-rollback-off.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/advance-shadow.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-keys.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-status.txt create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-revision-state.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/authz-143-negative.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/delivery-status-after-projection.txt create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/events-ready-shadow-notify.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/health-live-notify.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/health-shadow-notify.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/inventory-after-quarantine.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-advance.txt create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-notify.txt create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-projection.txt create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/notify-live-again.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/notify-live.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/notify-rollback-off.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/notify-shadow.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/projection-live-again.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/projection-live.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/projection-rollback-off.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/projection-shadow.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/quarantine.json create mode 100644 .doc/test-reports/assets/2026-09-15-event-shared-live/unauth-6275.json diff --git a/.doc/ops/long-term/2026-09-15-event-shared-live-cutover.md b/.doc/archive/ops/2026-09-15-event-shared-live-cutover.md similarity index 90% rename from .doc/ops/long-term/2026-09-15-event-shared-live-cutover.md rename to .doc/archive/ops/2026-09-15-event-shared-live-cutover.md index cbb70932..cef45874 100644 --- a/.doc/ops/long-term/2026-09-15-event-shared-live-cutover.md +++ b/.doc/archive/ops/2026-09-15-event-shared-live-cutover.md @@ -1,6 +1,6 @@ --- title: "共享环境事件消费者 live 切换" -status: planned +status: done created: 2026-09-15 updated: 2026-09-15 --- @@ -25,7 +25,7 @@ Track E 已在隔离库证明 notify / workflow_advance / projection 的 exactly ## 暂不启动原因 -共享 live 会改变 :8080 真实副作用路径。未获授权前保持 dispatcher/consumer `off`。 +已完成共享 live 与生产去注册。方法本体保留一个窗口。 ## 相关链接 diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 2e53c00f..be060a22 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,8 +5,8 @@ ## 焦点 -1. **TRACK-E LIVE**: 共享 consumer 切换([short-term](./short-term/2026-09-15-event-shared-live-cutover.md)) -2. **TRACK-P**: 立项完成([H0](./research/2026-09-15-http-error-envelope-h0-inventory.md) / [P0](./research/2026-09-15-pi-typed-result-p0-matrix.md));实施见 [长期任务](./long-term/2026-09-15-track-p-protocol-implementation.md) +1. **TRACK-P**: HTTP 信封与 PI typed-result 实施([long-term](./long-term/2026-09-15-track-p-protocol-implementation.md)) +2. **TRACK-E LIVE**: 共享 consumer 已 live;生产 hook 已去注册([projection](../test-reports/2026-09-15-event-projection-shared-live-test-report.md)) 3. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针) ## 并行认领 diff --git a/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md b/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md index 4b5ee859..a3d5075f 100644 --- a/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md +++ b/.doc/ops/short-term/2026-09-15-event-shared-live-cutover.md @@ -44,7 +44,7 @@ mutex: events ## 相关链接 -- Long-term: [.doc/ops/long-term/2026-09-15-event-shared-live-cutover.md](../long-term/2026-09-15-event-shared-live-cutover.md) +- Long-term: [.doc/archive/ops/2026-09-15-event-shared-live-cutover.md](../../archive/ops/2026-09-15-event-shared-live-cutover.md) - Test report: (本波完成后补) ## 证据 / 备注 diff --git a/.doc/remediation/gap-matrix.md b/.doc/remediation/gap-matrix.md index 6a8dd3e7..530ed0de 100644 --- a/.doc/remediation/gap-matrix.md +++ b/.doc/remediation/gap-matrix.md @@ -70,7 +70,7 @@ | DOCREV-ACTIVE-DOC-ROUTING | UI-05「当前受控文档」被法规关键词改路由到 `feasibility_reg`,intake 无 pending | **PASS** | [audit](../audit/2026-09-13-document-revision-active-doc-routing.md);Round20 UI-05 pending 停在 intake V2 | **不改弱 E2E**;不删证据 | | DOCREV-QUALITY-STATUS-FP | 行动计划表状态列「待续」被门禁当敷衍,两轮 repair 后 quality_failed 80 | **PASS** | [audit](../audit/2026-09-13-document-revision-quality-status-false-positive.md);Round20 UI-03 | **不删禁词、不放宽正文**;不删证据 | | GOVERNANCE-ACTION-AUTHZ | 文档动作按职责鉴权;未激活 202-blocked;关闭创建直通 | **PASS** | [test-report](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[audit 异态](../audit/2026-09-14-planning-with-workflow-key-anomaly.md) | 不改 83 条 planning+key 夹具;none 长 PI 审批链另开 | -| DOC-RELIABILITY-PILOT | 受控文档写路径与 PG 事件交付尚未形成生产闭环;闭包副作用在崩溃/重启后不可重试审计,错误 scope 或 projection 缺失仍可能导致刷新失败 | OPEN | [初审](../audit/2026-09-14-module-decoupling-plan-review.md);[Phase 1](./2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 复核](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订](./2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[U11–U12 报告](../test-reports/2026-09-15-app-composition-u11-u12-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[live 长期任务](../ops/long-term/2026-09-15-event-shared-live-cutover.md) | E0A–E2 隔离库已过。共享消费者未 live,故不删 hook API。dispatcher 保持 off。FAV、HTTP/PI 实施不纳入本 blocker 关闭条件。项目 143 只读 | +| DOC-RELIABILITY-PILOT | 受控文档写路径与 PG 事件交付尚未形成生产闭环;闭包副作用在崩溃/重启后不可重试审计,错误 scope 或 projection 缺失仍可能导致刷新失败 | **PASS** | [初审](../audit/2026-09-14-module-decoupling-plan-review.md);[Phase 1](./2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 复核](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订](./2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[U11–U12 报告](../test-reports/2026-09-15-app-composition-u11-u12-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[notify live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[advance live](../test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md);[projection live](../test-reports/2026-09-15-event-projection-shared-live-test-report.md) | 共享 notify/workflow_advance/projection 已 live;生产不再注册 `OnOutbox=` / `AddOnApprovedHook(`。FAV 仍 off。HTTP/PI 实施不纳入本行。项目 143 只读 | --- diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 63c6a63b..a21de60f 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -27,7 +27,7 @@ | PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` | | 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` | | CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md) | CI `TEST_DATABASE_URL` 隔离库仍缺;文档运行时真实 UI 已按 UI_PHASE 分文件且 post-restart 断言最终 A1 正文。`-race` 全包并行 TempDir flake 为基建残留 | `ci` | -| 模块解耦与可靠事件 | 文档可靠性纵向试点、PG 持久事件交付与选择性模块化 | partial | [架构](../system-design/2026-09-14-module-decoupling-architecture.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md);[Phase 1](../remediation/2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 审阅](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[E0A](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md);[E0B](../test-reports/2026-09-15-event-e0b-dispatcher-pg-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[D6](../test-reports/2026-09-15-doccontent-canonical-boundary-test-report.md);[D7a](../test-reports/2026-09-15-intent-bizdev-boundary-test-report.md);[D5](../test-reports/2026-09-15-stageflow-type-ownership-test-report.md);[D4](../test-reports/2026-09-15-bizdev-locator-removal-test-report.md);[D7b](../test-reports/2026-09-15-chatflow-orchestration-boundary-test-report.md);[H0](../ops/research/2026-09-15-http-error-envelope-h0-inventory.md);[P0](../ops/research/2026-09-15-pi-typed-result-p0-matrix.md);[收口](../test-reports/2026-09-15-phase2-closeout-test-report.md) | E0A–E2 隔离库已过;dispatcher 默认 off。D4–D7b 已合入。共享 live 与删 hook 见 [长期任务](../ops/long-term/2026-09-15-event-shared-live-cutover.md)。Track P 立项完成(H0/P0),实施另列 | `architecture-docs` | +| 模块解耦与可靠事件 | 文档可靠性纵向试点、PG 持久事件交付与选择性模块化 | partial | [架构](../system-design/2026-09-14-module-decoupling-architecture.md);[ADR](../adr/2026-09-14-modular-monolith-decoupling.md);[Phase 1](../remediation/2026-09-14-module-decoupling-refactor-plan.md);[Phase 2 审阅](../audit/2026-09-15-module-decoupling-phase2-plan-review.md);[Phase 2 修订计划](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md);[E0A](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md);[E0B](../test-reports/2026-09-15-event-e0b-dispatcher-pg-test-report.md);[E1A](../test-reports/2026-09-15-event-e1a-notify-cutover-test-report.md);[E1B](../test-reports/2026-09-15-event-e1b-workflow-advance-test-report.md);[E1C](../test-reports/2026-09-15-event-e1c-projection-test-report.md);[E2](../test-reports/2026-09-15-event-e2-hook-guard-test-report.md);[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[D6](../test-reports/2026-09-15-doccontent-canonical-boundary-test-report.md);[D7a](../test-reports/2026-09-15-intent-bizdev-boundary-test-report.md);[D5](../test-reports/2026-09-15-stageflow-type-ownership-test-report.md);[D4](../test-reports/2026-09-15-bizdev-locator-removal-test-report.md);[D7b](../test-reports/2026-09-15-chatflow-orchestration-boundary-test-report.md);[H0](../ops/research/2026-09-15-http-error-envelope-h0-inventory.md);[P0](../ops/research/2026-09-15-pi-typed-result-p0-matrix.md);[收口](../test-reports/2026-09-15-phase2-closeout-test-report.md);[notify live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[advance live](../test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md);[projection live](../test-reports/2026-09-15-event-projection-shared-live-test-report.md) | E0A–E2 与共享 live 已过;生产无 hook 注册。D4–D7b 已合入。FAV off。Track P 立项完成(H0/P0),实施另列 | `architecture-docs` | --- diff --git a/.doc/test-reports/2026-09-15-event-e2-unhook-production-test-report.md b/.doc/test-reports/2026-09-15-event-e2-unhook-production-test-report.md new file mode 100644 index 00000000..4f631e70 --- /dev/null +++ b/.doc/test-reports/2026-09-15-event-e2-unhook-production-test-report.md @@ -0,0 +1,47 @@ +# E2 生产去注册测试报告 + +## 一、 测试背景 (Background) + +三条消费者已在共享 `agent-dev` live。Track E DoD 要求生产组合根不再接线 `OnOutbox =` / `AddOnApprovedHook(`,但保留方法本体一个窗口。 + +## 二、 测试目标 (Test Objectives) + +- `internal/app/app.go` 与 server 组合根无上述注册。 +- `AddOnApprovedHook` / `OnOutbox` 字段与 Bind 辅助仍存在,测试可接线。 +- 授权读取 6275 仍与 digest 一致。 + +## 三、 测试方案与环境 (Test Plan & Environment) + +| 项 | 值 | +|----|-----| +| 分支 | `feature/event-shared-live-cutover` | +| 单测 | `TestProductionCompositionHasNoHookRegistration` | +| arch-check | `--mode block` | + +## 四、 预期结果 (Expected Results) + +组合根无注册;arch-check PASS;6275 SHA 不变。 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 5.1 源码门 + +- **测试输入 (Input)**:`go test ./internal/app -run TestProductionCompositionHasNoHookRegistration`;`./scripts/arch-check.sh --mode block` +- **实际输出 (Actual Output)**:PASS;`arch-check block: production gate passed` +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:arch-check ~0.2s;server 包 10s + +### 5.2 运行时(rebuild 后补) + +见同目录资产与 rebuild 日志。未登录仍 401;跨项目 143 仍 404 `DOCUMENT_NOT_FOUND`。 + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +| 命令 | 结果 | +|------|------| +| `go test -short ./internal/app ./internal/server ./internal/app/docnotify` | PASS | +| arch-check block | PASS | + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +`DOC-RELIABILITY-PILOT` 事件部分关闭。Track P 独立实施。hook 方法本体尚未删除。 diff --git a/.doc/test-reports/2026-09-15-event-notify-shared-live-test-report.md b/.doc/test-reports/2026-09-15-event-notify-shared-live-test-report.md new file mode 100644 index 00000000..e073ecf1 --- /dev/null +++ b/.doc/test-reports/2026-09-15-event-notify-shared-live-test-report.md @@ -0,0 +1,61 @@ +# 共享 notify live 测试报告 + +## 一、 测试背景 (Background) + +隔离库 E1A 已证明 notify 的 shadow dry-run、唯一可见通知与回滚。本波在共享 `agent_system_dev` 上把 `notify` 切为权威写者。 + +## 二、 测试目标 (Test Objectives) + +- fav / test / main 切模式失败。 +- 历史非法 fav/projection delivery 已 quarantine,不得 claim。 +- notify:shadow → drain(重启 events worker)→ live → off 回滚 → 再 live。 +- FAV 保持 off;HTTP `:8080` 与 events `:8088` ready。 + +## 三、 测试方案与环境 (Test Plan & Environment) + +| 项 | 值 | +|----|-----| +| OS | Linux | +| 分支 | `feature/event-shared-live-cutover` | +| 共享库 | `shuimu-context-pg-dev` / `agent_system_dev` schema 43 | +| 隔离回归 | `127.0.0.1:5434/agent_rel_shared_live` | +| 项目 143 | 只读 | + +## 四、 预期结果 (Expected Results) + +notify revision 递增;最终 mode=live;fav=off;闭包在 live 时 `WriterLive` 为真。 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 5.1 隔离库 E1A + +- **测试输入 (Input)**:`TEST_DATABASE_URL=.../agent_rel_shared_live`;`go test ./internal/app/docnotify -run 'TestE1A_|TestIntended'` +- **实际输出 (Actual Output)**:`ok chat-agent-system/internal/app/docnotify 0.361s` +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:0.36s + +### 5.2 共享切模式 + +- **测试输入 (Input)**:`./scripts/event-consumer-mode.sh set notify {shadow,live,off,live}` +- **实际输出 (Actual Output)**:shadow rev=2 → live rev=3 → off rev=4 → live rev=5。见 `assets/2026-09-15-event-shared-live/notify-*.json` +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:约 7s(含 worker 重启) + +### 5.3 健康检查 + +- **测试输入 (Input)**:`curl :8080/ready` `:8088/ready` +- **实际输出 (Actual Output)**:schema 43;events ready ok;fav mode=off +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:<1s + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +| 命令 | 结果 | +|------|------| +| ValidateOpsCutover 单测 | PASS | +| E1A 隔离 PG | PASS | +| 共享 notify 回滚演练 | PASS | + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +共享 **notify=live**。下一步 workflow_advance。未删 hook API。 diff --git a/.doc/test-reports/2026-09-15-event-projection-shared-live-test-report.md b/.doc/test-reports/2026-09-15-event-projection-shared-live-test-report.md new file mode 100644 index 00000000..fdbbee68 --- /dev/null +++ b/.doc/test-reports/2026-09-15-event-projection-shared-live-test-report.md @@ -0,0 +1,57 @@ +# 共享 projection live 测试报告 + +## 一、 测试背景 (Background) + +隔离库 E1C 已证明 projection no-replace。本波在共享库切 live,并做三类 worker 重启与授权读取。 + +## 二、 测试目标 (Test Objectives) + +- projection shadow → 重启 → live → 重启 → lease 过期后再重启 → 回滚 off → 再 live +- 非法 taskrun projection delivery 保持 pending(quarantine 不可 claim) +- 项目 6275 授权刷新正文 SHA 与 `document_records.content_sha256` 一致 +- 项目 143 跨项目读取不泄露;未登录 401 +- FAV off + +## 三、 测试方案与环境 (Test Plan & Environment) + +| 项 | 值 | +|----|-----| +| 共享 | schema 43;events `:8088` | +| 隔离 | `agent_rel_shared_live` | +| 授权用户 | `zhanggong`(6275 owner,非 143 成员) | +| 浏览器 | 无独立 browser MCP;用 HTTP 刷新代替页面点击 | + +## 四、 预期结果 (Expected Results) + +三消费者 live;fav off;6275 SHA 一致;143 为 `DOCUMENT_NOT_FOUND`。 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 5.1 隔离 E1C + +- **测试输入 (Input)**:`go test ./internal/app/docproject -run TestE1C_` +- **实际输出 (Actual Output)**:`ok 0.039s` +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:0.039s + +### 5.2 共享切换与三类重启 + +- **测试输入 (Input)**:shadow;restart1;live;restart2;sleep 31;restart3;off;live +- **实际输出 (Actual Output)**:projection rev 2→3→4→5;`modes-after-projection.txt` 三条业务消费者均为 live,fav off。quarantine projection 仍 pending=2 +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:约 45s + +### 5.3 授权刷新 / 负例 + +- **测试输入 (Input)**:登录后两次 GET `/api/v1/docgov/documents/doc_proj6275_intake/content`;跨项目 GET 143 文档;无 token GET 6275 +- **实际输出 (Actual Output)**:200/200,`content_sha256=dc99aa13…641ddf` 与库一致;143 → 404 `DOCUMENT_NOT_FOUND`;未登录 401 +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:<1s + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +E1C 隔离 PASS;共享 live + 重启 + HTTP 刷新 PASS。无浏览器截图(工具不可用)。 + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +三条消费者均 **live**。下一步删除生产 hook 注册。未写项目 143。 diff --git a/.doc/test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md b/.doc/test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md new file mode 100644 index 00000000..9aaaacb4 --- /dev/null +++ b/.doc/test-reports/2026-09-15-event-workflow-advance-shared-live-test-report.md @@ -0,0 +1,51 @@ +# 共享 workflow_advance live 测试报告 + +## 一、 测试背景 (Background) + +隔离库 E1B 已证明阶段推进 exactly-one visible outcome。本波在共享库将 `workflow_advance` 切 live。 + +## 二、 测试目标 (Test Objectives) + +shadow → drain → live → off 回滚 → 再 live;notify 保持 live;FAV off。 + +## 三、 测试方案与环境 (Test Plan & Environment) + +| 项 | 值 | +|----|-----| +| 共享库 | `agent_system_dev` schema 43 | +| 隔离回归 | `agent_rel_shared_live` | + +## 四、 预期结果 (Expected Results) + +workflow_advance 最终 live revision≥5;notify 仍 live;fav off。 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 5.1 隔离 E1B + +- **测试输入 (Input)**:`go test ./internal/app/docadvance -run TestE1B_` +- **实际输出 (Actual Output)**:`ok 0.065s` +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:0.065s + +### 5.2 共享切换 + +- **测试输入 (Input)**:`event-consumer-mode.sh set workflow_advance {shadow,live,off,live}` +- **实际输出 (Actual Output)**:rev 2→3→4→5;modes-after-advance.txt:notify live、workflow_advance live、projection off、fav off +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:约 7s + +### 5.3 健康检查 + +- **测试输入 (Input)**:`:8080/ready` `:8088/ready` +- **实际输出 (Actual Output)**:schema 43 ok +- **评价判定 (Evaluation / Verdict)**:**PASS** +- **性能与耗时 (Duration & Latency)**:<1s + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +E1B 隔离 PASS;共享回滚演练 PASS。 + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +**workflow_advance=live**。下一步 projection + 授权页。 diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/__pycache__/dual-tab-live.cpython-314.pyc b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/__pycache__/dual-tab-live.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7310a84cc4ec6aa3c60f2387b286e0a0af9d7bf8 GIT binary patch literal 27082 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�^pFRf!gEf59>^hV=vAfSTeQ#%(ryP#`K>HpQ4w}Uj*ailBiXDAI>d` z+Qjg7K-?+*U+Aq<3`V3C9@v68BJKsN{d z+Vnss3N~M_3RDIAL&bM9P4^NMV8>_@kRnb1J0_0+1u8>&lcd0*!t|bEN&!bB;hfP$ zX(r}1k$?M&2Ckn9Yx01CwFSZAu(lLzHeF$`Hmut`+4l3?!m#1w)uRDX(00>t!x3tI zZFJVRSkmxv!MyEV#}6F~9pP4rIo`L}+IOYEUvTy42pRkhS6VK&1j^n^DZb@cOs-w*ITqe|9Kr?LoZ$zl zZ>P>(d~a6=ye~p(@^KIfain1sg!%0!-&HRiwlj`1%ZJb2IeZpIdk&C{(G8f+yVV*t zlDAre=VonhIlkqXYyI}72ckTZPPkw0nAGf_$>&K@uU;zv%TlmZUkqPD;5_e!2Wg)#a*SZaB#_ z*%{Gi1d175{$$%P6-kqqR&2?E1@HU)~)|zMEVelHN_K2Qzl};BH1)$b2__TR6FTO8HBD zb|gE0>fjpv2zS|~-%E_wkH8Ry>wULJz*OL5Z7}0*dKtzca2J|mOhmL>XE&pug5x+u zHuqE-#BmystyzyEdDazzwM)`=kVLOPFdYB}0%pd?x0pqY7NP1D40;_SwY@WkddT}DaHFRiGlk|QeSopR#oBMoC>)2B)c}8 zUKek}#>D5Qpa%)g)QA$8_g=DEC!cJ6kcOnE+uyn*&0dkJUM!v|f4V9`;=kectswE0~AfZ`86q-fuec)nwzcizvVW0F@(q#O|k)y{4@~nnNsq}*qc&Owb z$uk-nbr~NO${cgq6K9utF~GP zz4&S;D=mg^oPQNNbe}=5g%H4&0DS#>75Kr&X*v%j>7q%|3*oUln=V66HsrMi?-V$9 zoi4fn)scgeb5W3of_xMpZ!rzKjbS;|*6+sGxEUYg3Tx}qF^T~ zg*@VGJz@ssb`)7k453Gu%doh(Xgm! zm@f(|S|_D27nnIdeH>k%&g4{t&W01}C)H~bhvBMoNK8yZH#=R?UDh%A)w7-9#Ql>A zYZ4EH6Zb))N;8u+oi&p;ofnZBB5-?8+cRy?9e(DpzY$#rdgbut!@>4&N@ZADbzde? ziznsazkq(8dFIS>W6zBFj|Y=(rr$`vnR6p2G&p-UT+kSnH%&?+N;SI52i-I3rviOp zW%;Bml9J94h6nOAQIqJ?YME%)r-?F={?o)nk>S%46$BX?QPQV5$)fF_R!Bu@pX&N$ eqS|{0G=x+;g3QY*~VrCIn=ss9Hl31nOV literal 0 HcmV?d00001 diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-live-again.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-live-again.json new file mode 100644 index 00000000..7c126584 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-live-again.json @@ -0,0 +1 @@ +{"after":"live","before":"off","consumer":"workflow_advance","environment":"dev","revision":5} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-live.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-live.json new file mode 100644 index 00000000..837c067f --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-live.json @@ -0,0 +1 @@ +{"after":"live","before":"shadow","consumer":"workflow_advance","environment":"dev","revision":3} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-rollback-off.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-rollback-off.json new file mode 100644 index 00000000..a0c4a467 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-rollback-off.json @@ -0,0 +1 @@ +{"after":"off","before":"live","consumer":"workflow_advance","environment":"dev","revision":4} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-shadow.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-shadow.json new file mode 100644 index 00000000..1060543c --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/advance-shadow.json @@ -0,0 +1 @@ +{"after":"shadow","before":"off","consumer":"workflow_advance","environment":"dev","revision":2} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-keys.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-keys.json new file mode 100644 index 00000000..80713282 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-keys.json @@ -0,0 +1,15 @@ +{ + "status": 200, + "keys": [ + "consistency", + "content_sha256", + "doc_id", + "file_sha256", + "head_sha256", + "is_template_only", + "markdown", + "record_sha256", + "version" + ], + "content_sha256": "dc99aa130644353f258e951887c9d3babf35afc9d46e008327121bdedc641ddf" +} \ No newline at end of file diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-status.txt b/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-status.txt new file mode 100644 index 00000000..37a03d64 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-content-status.txt @@ -0,0 +1 @@ +status=200 diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-revision-state.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-revision-state.json new file mode 100644 index 00000000..0f14b537 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/auth-6275-revision-state.json @@ -0,0 +1,18 @@ +{ + "status": 200, + "head_revision_no": 0, + "keys": [ + "baseline_snapshot_id", + "can_decide_pending", + "capabilities", + "current_version_no", + "doc_id", + "etag", + "head_block_map", + "head_revision_no", + "head_sha256", + "head_snapshot_id", + "manual_change_count", + "version_status" + ] +} \ No newline at end of file diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/authz-143-negative.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/authz-143-negative.json new file mode 100644 index 00000000..cdb670f1 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/authz-143-negative.json @@ -0,0 +1,5 @@ +{ + "status": 404, + "doc_id_redacted": true, + "body_prefix": "{\"error_code\":\"DOCUMENT_NOT_FOUND\",\"message\":\"document not found\"}\n" +} \ No newline at end of file diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/delivery-status-after-projection.txt b/.doc/test-reports/assets/2026-09-15-event-shared-live/delivery-status-after-projection.txt new file mode 100644 index 00000000..014463ce --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/delivery-status-after-projection.txt @@ -0,0 +1,6 @@ + consumer | status | count +------------+---------+------- + fav | pending | 2 + projection | pending | 2 +(2 rows) + diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/events-ready-shadow-notify.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/events-ready-shadow-notify.json new file mode 100644 index 00000000..13109c02 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/events-ready-shadow-notify.json @@ -0,0 +1 @@ +{"status":"ok"} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/health-live-notify.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/health-live-notify.json new file mode 100644 index 00000000..de36335c --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/health-live-notify.json @@ -0,0 +1 @@ +{"status":"ok","env":"dev","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432,"database_ready":true,"schema_version":43,"schema_ready":true,"task_store_ready":true} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/health-shadow-notify.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/health-shadow-notify.json new file mode 100644 index 00000000..de36335c --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/health-shadow-notify.json @@ -0,0 +1 @@ +{"status":"ok","env":"dev","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432,"database_ready":true,"schema_version":43,"schema_ready":true,"task_store_ready":true} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/inventory-after-quarantine.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/inventory-after-quarantine.json new file mode 100644 index 00000000..67cd608a --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/inventory-after-quarantine.json @@ -0,0 +1 @@ +{"backlog":[{"consumer":"fav","status":"pending","count":2},{"consumer":"projection","status":"pending","count":2}],"modes":[{"environment":"dev","consumer":"fav","mode":"off","revision":1},{"environment":"dev","consumer":"notify","mode":"off","revision":1},{"environment":"dev","consumer":"projection","mode":"off","revision":1},{"environment":"dev","consumer":"workflow_advance","mode":"off","revision":1},{"environment":"main","consumer":"fav","mode":"off","revision":1},{"environment":"main","consumer":"notify","mode":"off","revision":1},{"environment":"main","consumer":"projection","mode":"off","revision":1},{"environment":"main","consumer":"workflow_advance","mode":"off","revision":1},{"environment":"test","consumer":"fav","mode":"off","revision":1},{"environment":"test","consumer":"notify","mode":"off","revision":1},{"environment":"test","consumer":"projection","mode":"off","revision":1},{"environment":"test","consumer":"workflow_advance","mode":"off","revision":1}],"pending_classifications":[{"event_id":"taskrun:run_1789452300651535257:failed","consumer":"fav","classification":"quarantine","reason":"unknown_or_disabled_consumer"},{"event_id":"taskrun:run_1789452300651535257:failed","consumer":"projection","classification":"quarantine","reason":"unknown_or_disabled_consumer"},{"event_id":"taskrun:run_1789452602295343365:completed","consumer":"fav","classification":"quarantine","reason":"unknown_or_disabled_consumer"},{"event_id":"taskrun:run_1789452602295343365:completed","consumer":"projection","classification":"quarantine","reason":"unknown_or_disabled_consumer"}]} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-advance.txt b/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-advance.txt new file mode 100644 index 00000000..1941ed9e --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-advance.txt @@ -0,0 +1,8 @@ + consumer | mode | revision +------------------+------+---------- + fav | off | 1 + notify | live | 5 + projection | off | 1 + workflow_advance | live | 5 +(4 rows) + diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-notify.txt b/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-notify.txt new file mode 100644 index 00000000..bfc6b9b4 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-notify.txt @@ -0,0 +1,8 @@ + environment | consumer | mode | revision +-------------+------------------+------+---------- + dev | fav | off | 1 + dev | notify | live | 5 + dev | projection | off | 1 + dev | workflow_advance | off | 1 +(4 rows) + diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-projection.txt b/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-projection.txt new file mode 100644 index 00000000..c4ac9951 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/modes-after-projection.txt @@ -0,0 +1,8 @@ + consumer | mode | revision +------------------+------+---------- + fav | off | 1 + notify | live | 5 + projection | live | 5 + workflow_advance | live | 5 +(4 rows) + diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-live-again.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-live-again.json new file mode 100644 index 00000000..fcbd69f3 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-live-again.json @@ -0,0 +1 @@ +{"after":"live","before":"off","consumer":"notify","environment":"dev","revision":5} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-live.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-live.json new file mode 100644 index 00000000..35e2048a --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-live.json @@ -0,0 +1 @@ +{"after":"live","before":"shadow","consumer":"notify","environment":"dev","revision":3} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-rollback-off.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-rollback-off.json new file mode 100644 index 00000000..70431645 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-rollback-off.json @@ -0,0 +1 @@ +{"after":"off","before":"live","consumer":"notify","environment":"dev","revision":4} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-shadow.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-shadow.json new file mode 100644 index 00000000..eefd4b37 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/notify-shadow.json @@ -0,0 +1 @@ +{"after":"shadow","before":"off","consumer":"notify","environment":"dev","revision":2} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-live-again.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-live-again.json new file mode 100644 index 00000000..b181c76d --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-live-again.json @@ -0,0 +1 @@ +{"after":"live","before":"off","consumer":"projection","environment":"dev","revision":5} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-live.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-live.json new file mode 100644 index 00000000..ea57ad38 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-live.json @@ -0,0 +1 @@ +{"after":"live","before":"shadow","consumer":"projection","environment":"dev","revision":3} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-rollback-off.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-rollback-off.json new file mode 100644 index 00000000..8cf5993e --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-rollback-off.json @@ -0,0 +1 @@ +{"after":"off","before":"live","consumer":"projection","environment":"dev","revision":4} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-shadow.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-shadow.json new file mode 100644 index 00000000..41977e2e --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/projection-shadow.json @@ -0,0 +1 @@ +{"after":"shadow","before":"off","consumer":"projection","environment":"dev","revision":2} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/quarantine.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/quarantine.json new file mode 100644 index 00000000..960be91f --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/quarantine.json @@ -0,0 +1 @@ +{"quarantined":4} diff --git a/.doc/test-reports/assets/2026-09-15-event-shared-live/unauth-6275.json b/.doc/test-reports/assets/2026-09-15-event-shared-live/unauth-6275.json new file mode 100644 index 00000000..04c9a8bb --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-event-shared-live/unauth-6275.json @@ -0,0 +1,4 @@ +{ + "status": 401, + "body": "{\"error_code\":\"ERR_UNAUTHENTICATED\",\"message\":\"请先登录\"}\n" +} \ No newline at end of file From 7a9d0d850d981286b70c71d6b88c61df8d3c383d Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 15 Sep 2026 14:24:17 +0000 Subject: [PATCH 4/4] chore: ignore pycache and release the live-cutover integration slot Shared live rebuild passed health/ready; keep Python bytecode out of test-report assets. Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 4 ++-- .../__pycache__/dual-tab-live.cpython-314.pyc | Bin 27082 -> 0 bytes 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 .doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/__pycache__/dual-tab-live.cpython-314.pyc diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index be060a22..974356bb 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -70,7 +70,7 @@ | Cursor | bizdev-locator-removal | fix/bizdev-locator-removal | - | bizdev | done | | Cursor | chatflow-orchestration-boundary | fix/chatflow-orchestration-boundary | - | chat | done | | Cursor | phase2-closeout-inventories | feature/phase2-closeout-inventories | - | architecture-docs | done | -| Cursor | event-shared-live-cutover | feature/event-shared-live-cutover | - | events | in_progress | +| Cursor | event-shared-live-cutover | feature/event-shared-live-cutover | - | events | done | > 新任务开始前登记。`branch` 必须为 `feature/` 或 `fix/`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 @@ -82,7 +82,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| Cursor | e2e | 2026-09-15 | shared event consumer live cutover;FAV off | - | +| - | idle | - | - | - | **phase**: `idle` | `merging` | `smoke` | `e2e` diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/__pycache__/dual-tab-live.cpython-314.pyc b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/__pycache__/dual-tab-live.cpython-314.pyc deleted file mode 100644 index 7310a84cc4ec6aa3c60f2387b286e0a0af9d7bf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27082 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�^pFRf!gEf59>^hV=vAfSTeQ#%(ryP#`K>HpQ4w}Uj*ailBiXDAI>d` z+Qjg7K-?+*U+Aq<3`V3C9@v68BJKsN{d z+Vnss3N~M_3RDIAL&bM9P4^NMV8>_@kRnb1J0_0+1u8>&lcd0*!t|bEN&!bB;hfP$ zX(r}1k$?M&2Ckn9Yx01CwFSZAu(lLzHeF$`Hmut`+4l3?!m#1w)uRDX(00>t!x3tI zZFJVRSkmxv!MyEV#}6F~9pP4rIo`L}+IOYEUvTy42pRkhS6VK&1j^n^DZb@cOs-w*ITqe|9Kr?LoZ$zl zZ>P>(d~a6=ye~p(@^KIfain1sg!%0!-&HRiwlj`1%ZJb2IeZpIdk&C{(G8f+yVV*t zlDAre=VonhIlkqXYyI}72ckTZPPkw0nAGf_$>&K@uU;zv%TlmZUkqPD;5_e!2Wg)#a*SZaB#_ z*%{Gi1d175{$$%P6-kqqR&2?E1@HU)~)|zMEVelHN_K2Qzl};BH1)$b2__TR6FTO8HBD zb|gE0>fjpv2zS|~-%E_wkH8Ry>wULJz*OL5Z7}0*dKtzca2J|mOhmL>XE&pug5x+u zHuqE-#BmystyzyEdDazzwM)`=kVLOPFdYB}0%pd?x0pqY7NP1D40;_SwY@WkddT}DaHFRiGlk|QeSopR#oBMoC>)2B)c}8 zUKek}#>D5Qpa%)g)QA$8_g=DEC!cJ6kcOnE+uyn*&0dkJUM!v|f4V9`;=kectswE0~AfZ`86q-fuec)nwzcizvVW0F@(q#O|k)y{4@~nnNsq}*qc&Owb z$uk-nbr~NO${cgq6K9utF~GP zz4&S;D=mg^oPQNNbe}=5g%H4&0DS#>75Kr&X*v%j>7q%|3*oUln=V66HsrMi?-V$9 zoi4fn)scgeb5W3of_xMpZ!rzKjbS;|*6+sGxEUYg3Tx}qF^T~ zg*@VGJz@ssb`)7k453Gu%doh(Xgm! zm@f(|S|_D27nnIdeH>k%&g4{t&W01}C)H~bhvBMoNK8yZH#=R?UDh%A)w7-9#Ql>A zYZ4EH6Zb))N;8u+oi&p;ofnZBB5-?8+cRy?9e(DpzY$#rdgbut!@>4&N@ZADbzde? ziznsazkq(8dFIS>W6zBFj|Y=(rr$`vnR6p2G&p-UT+kSnH%&?+N;SI52i-I3rviOp zW%;Bml9J94h6nOAQIqJ?YME%)r-?F={?o)nk>S%46$BX?QPQV5$)fF_R!Bu@pX&N$ eqS|{0G=x+;g3QY*~VrCIn=ss9Hl31nOV