diff --git a/.doc/audit/2026-09-16-fav-u7-remaining-port.md b/.doc/audit/2026-09-16-fav-u7-remaining-port.md index 2ace9937..c1abcc4a 100644 --- a/.doc/audit/2026-09-16-fav-u7-remaining-port.md +++ b/.doc/audit/2026-09-16-fav-u7-remaining-port.md @@ -2,7 +2,7 @@ title: "FAV U7 incremental — remaining port inventory" status: open created: 2026-09-16 -updated: 2026-09-16 +updated: 2026-09-17 --- # FAV U7 增量:未移植清单 @@ -17,5 +17,8 @@ updated: 2026-09-16 | ingest hooks | `internal/server/vault.go` OnFinalArtifact* | 会写 staging;需与 publisher 门闩同批审批 | | schema JSON | `*.schema.json` | 可选文档资产 | | durable Register(fav) | `cmd/events` | events 硬门 + 单独 cutover 审批 | +| live staging → artifact → publication | publisher Drain / ingest hooks | 每级观察窗 ≥7 天;本轮 **未** 放量 | + +**W6 已移植(仍非 live)**:`internal/app/fav.EventConsumer.ObserveShadow` 只计算 intended outcome(`WouldStage`/`WouldPublish` 恒 false);`FAV_DEGRADED_READ` 为 true 时 shadow 直接拒绝;`HandleLease` 与 `Registry.Register(fav)` 仍 fail-closed。 **红线保持**:`FAV_DEGRADED_READ=false`;fav consumer mode=off;禁止整支 merge。 diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 64e7ca28..3495cb5f 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,7 +5,7 @@ ## 焦点 -1. **partial/deferred 全量收口**:W1–W4 **done**;W5a **done**(矩阵自举行仍 partial);续 W6–W7 +1. **partial/deferred 全量收口**:W1–W4 **done**;W5a **done**(矩阵自举行仍 partial);W6 **shadow done**(矩阵 FAV 仍 partial,live 未放量);续 W7 2. **运维**:nightly `DATA_DIR=dev` 已对齐;KB 宿主机分层加载 / fail-closed **done**(`fix/kb-host-auth-layered-load`) 3. **红线**:禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`(升 `confirm` 须 W4 闭环后独立授权);`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 顺序 staging→artifact→publication、每级观察窗证据驱动(下限 ≥7 天)、`FAV_DEGRADED_READ=false`;flag 默认 off;项目 143 只读 @@ -18,7 +18,7 @@ | Cursor | 2026-09-17-w3-pi-change-report-narrative | feature/w3-pi-change-report-narrative-live | - | docgov | done | | Cursor | 2026-09-17-w4-multi-intent-u9-u12 | feature/w4-u9-segment-live | - | intent-plan | done | | Cursor | 2026-09-17-w5a-software-bootstrap-ui | feature/w5a-software-bootstrap-ui-pi | - | workflow-design | done | -| Cursor | 2026-09-17-w6-fav-subscriber-shadow | feature/w6-fav-subscriber-shadow | - | events | planned | +| Cursor | 2026-09-17-w6-fav-subscriber-shadow | feature/w6-fav-subscriber-shadow | - | events | done | | Cursor | 2026-09-17-w7-d7-inventory | feature/w7-d7-data-inventory | - | chat | planned | | Cursor | 2026-09-17-kb-host-auth-fix | fix/kb-host-auth-layered-load | - | infra | done | | Cursor | 2026-09-17-w1-ci-e2e-green-nightly | feature/w1-ci-e2e-green-nightly | - | ci | done | @@ -118,7 +118,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| - | idle | - | - | W6–W7 | +| - | idle | - | - | W7 | **phase**: `idle` | `merging` | `smoke` | `e2e` diff --git a/.doc/ops/short-term/2026-09-16-partial-deferred-completion.md b/.doc/ops/short-term/2026-09-16-partial-deferred-completion.md index c05b62e4..d7d643e2 100644 --- a/.doc/ops/short-term/2026-09-16-partial-deferred-completion.md +++ b/.doc/ops/short-term/2026-09-16-partial-deferred-completion.md @@ -27,7 +27,7 @@ mutex: architecture-docs 3. [x] W3 PU7 PI change-report 叙事(mutex `docgov`;开 flag + 真实 PI + fallback 负例 + UI 区分) 4. [x] W4 PU8 U9–U12(mutex `intent-plan`;逐 action 独立 flag,U9→U12 顺序;升 confirm 另开独立授权,不升 full) 5. [ ] W5 纯软件自举(mutex `workflow-design`;SWBOOT-10 非 stub + 真实 PI + UI 二次验收) -6. [ ] W6 FAV(mutex `events`;subscriber 审计 → shadow → live staging→artifact→publication;每级观察窗 ≥7 天;`FAV_DEGRADED_READ=false`;禁止整支 merge) +6. [ ] W6 FAV(mutex `events`;**shadow 已合** → live staging→artifact→publication 仍未开;每级观察窗 ≥7 天;`FAV_DEGRADED_READ=false`;禁止整支 merge) 7. [ ] W7 D7 数据治理(mutex `chat`,排在 W2 后;盘点 → 按 ADR 口径整改;合规签字前数字 `pending-compliance-signoff`;违规升 gap-matrix) 8. [ ] 逐行升 done(保留 lineage)+ CURRENT / 契约状态同步 9. [x] §7 四项开放问题决策落盘(2026-09-17) diff --git a/.doc/ops/short-term/2026-09-17-w6-fav-subscriber-shadow.md b/.doc/ops/short-term/2026-09-17-w6-fav-subscriber-shadow.md new file mode 100644 index 00000000..d16bc65e --- /dev/null +++ b/.doc/ops/short-term/2026-09-17-w6-fav-subscriber-shadow.md @@ -0,0 +1,51 @@ +--- +title: "W6 FAV subscriber shadow" +status: done +created: 2026-09-17 +updated: 2026-09-17 +owner: Cursor +branch: feature/w6-fav-subscriber-shadow +worktree: "-" +mutex: events +--- + +# W6 FAV subscriber shadow + +## 目标 + +手工移植 FAV subscriber **shadow**:只计算 intended outcome,不写业务副作用;`FAV_DEGRADED_READ=false`;`FAV_PUBLISHER_ENABLED` 默认 false。矩阵保持 `partial`。 + +## 范围 + +- `ObserveShadow` 只读计算 +- vault status / CLI 标明 shadow 与 live 未放量 +- 负例:degraded read 拒绝;HandleLease / Register(fav) 仍 fail-closed + +## 非目标 + +- 整支 merge `feature/final-artifact-vault` +- live staging → artifact → publication +- 启用 `FAV_DEGRADED_READ` +- 升 FEATURE-MATRIX FAV 为 `done` + +## 步骤 + +1. [x] `ObserveShadow` + 负例单测 +2. [x] 冒烟 + 报告(live 未放量) +3. [x] 合 `dev`;矩阵保持 partial + +## 验收标准 + +- [x] WouldStage / WouldPublish 恒 false +- [x] FAV_DEGRADED_READ=true 时 shadow 拒绝 +- [x] 报告写清 shadow 证据与「live 未放量」 + +## 相关链接 + +- Remaining port: [../../audit/2026-09-16-fav-u7-remaining-port.md](../../audit/2026-09-16-fav-u7-remaining-port.md) +- U7 报告: [../../test-reports/2026-09-16-fav-u7-incremental-test-report.md](../../test-reports/2026-09-16-fav-u7-incremental-test-report.md) +- Test report: [../../test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md](../../test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md) + +## 证据 / 备注 + +不占 live 观察窗;≥7 天窗口仍属后续 live 切片。 diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index a640ca4f..8daa0ced 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -66,7 +66,7 @@ | CI / E2E | PR CI、PG 集成、治理 PG、已签真实 PI 门禁;**nightly** schedule/`workflow_dispatch` + infra/product 分类 receipt | done | 加固(阻止**下一次**晋升,不自动否定已发布) | [BUG-08](../test-reports/2026-09-16-ci-governance-pg-bug08-test-report.md);[PI 三工作流](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md);[W1 green](../test-reports/2026-09-17-w1-ci-e2e-green-nightly-test-report.md) + [receipt `6e2476f0…`](../assets/promotion-receipts/6e2476f0f2e408e111fe06ca7c31a38294a340b3.json) | **before**:partial(FullNone product_fail / secrets 文案过时)→ **after**:secrets 已配;duplicate goose 00048 已清;FullNone+U8 nightly `verdict=pass` | `ci` | | Artifact 同 digest 晋升 | 候选 commit 一次构建、按 digest 晋升、promotion receipt | done | 加固 | [计划 PU5B](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md);[Wave2B](../test-reports/2026-09-16-wave2b-nightly-promotion-receipt-test-report.md);[Wave2C](../test-reports/2026-09-16-wave2c-same-image-digest-test-report.md) | **关闭**:same-image pin(`candidate-images-*` + `PINNED_IMAGE_MODE` + promote `--require-digests`)。**声明**:单机 retag/digest pin;远程 registry 推送另立任务 | `infra` | | 可靠事件核心 | PG dispatcher、notify/advance/projection live、生产闭包 hook 退出、Phase2 选择性解耦已合入 | done | non_blocker | [E0A–E2](../test-reports/2026-09-15-event-e0a-production-scope-contract-test-report.md) 等;[复盘](../ops/research/2026-09-15-track-e-retrospective.md);[shared live](../test-reports/2026-09-15-event-notify-shared-live-test-report.md);[phase2 closeout](../test-reports/2026-09-15-phase2-closeout-test-report.md) | **before**:与 FAV off 混成 partial → **after**:FAV 独立行。选择性后续解耦按收益触发 | `architecture-docs` / `events` | -| Final Artifact Vault(FAV) | 不可变终态制品库;`FAV_DEGRADED_READ=false` | partial | deferred_enhancement | [Track E 复盘](../ops/research/2026-09-15-track-e-retrospective.md);[U7 报告](../test-reports/2026-09-16-fav-u7-incremental-test-report.md);[short-term](../ops/short-term/2026-09-16-fav-u7-incremental.md) | U7 增量已合 `dev`(publisher/consumer 默认 off);仍**禁止**整支 merge;`FAV_DEGRADED_READ` 默认 false | `events` | +| Final Artifact Vault(FAV) | 不可变终态制品库;`FAV_DEGRADED_READ=false` | partial | deferred_enhancement | [Track E 复盘](../ops/research/2026-09-15-track-e-retrospective.md);[U7 报告](../test-reports/2026-09-16-fav-u7-incremental-test-report.md);[W6 shadow](../test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md);[short-term](../ops/short-term/2026-09-17-w6-fav-subscriber-shadow.md) | **after W6**:subscriber `ObserveShadow` 只计算 intended outcome,不写副作用。仍**禁止**整支 merge;`FAV_DEGRADED_READ` 默认 false;**live 未放量**(staging→artifact→publication 各 ≥7 天观察窗) | `events` | --- diff --git a/.doc/test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md b/.doc/test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md new file mode 100644 index 00000000..f68a6dc1 --- /dev/null +++ b/.doc/test-reports/2026-09-17-w6-fav-subscriber-shadow-test-report.md @@ -0,0 +1,92 @@ +# W6 FAV subscriber shadow 测试报告 + +## 一、 测试背景 (Background) + +FAV 禁止整支 merge `feature/final-artifact-vault`。U7 增量已合 `dev`(publisher/consumer 默认 off)。W6 只手工移植 subscriber **shadow**:计算 intended outcome,不写 staging/artifact/publication,不开启 `FAV_DEGRADED_READ`。live 观察窗 ≥7 天,本轮不开。 + +## 二、 测试目标 (Test Objectives) + +- `ObserveShadow` 只读:`WouldStage`/`WouldPublish`/`DegradedReadUsed` 恒 false +- `FAV_DEGRADED_READ=true`(结构字段或进程 env)时 shadow 拒绝 +- `HandleLease` 与 `Registry.Register(fav)` 仍 fail-closed +- vault status / `artifact-vault status` 标明 `shadow_observe` 与 live 未放量 +- 仓库默认 `FAV_PUBLISHER_ENABLED=false`、`FAV_DEGRADED_READ=false` +- FEATURE-MATRIX FAV **保持 partial** + +## 三、 测试方案与环境 (Test Plan & Environment) + +| 项 | 值 | +|----|----| +| OS | Linux 7.0.0-31-generic | +| Go | go1.27.1 linux/amd64 | +| 分支 | `feature/w6-fav-subscriber-shadow`(自 `dev@49d901e`) | +| 层级 | 单元测试 + CLI status;无真实 PI(本波无 PI 路径) | +| 证据 | `.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/` | + +核心用例矩阵: + +| ID | 用例 | 期望 | +|----|------|------| +| S1 | 零值 consumer | DefaultEnabled=false;HandleLease → ErrDisabledConsumer | +| S2 | Enabled=true 仍无 registry 审批 | HandleLease 拒绝;Register(fav) 失败 | +| S3 | ObserveShadow | logical=`document:doc_1`;不声明 writes | +| S4 | AllowDegradedRead / env true | shadow error;不写 vault | +| S5 | GET vault/status | consumer_mode=off;would_stage/publish=false;live_not_opened=true | +| S6 | artifact-vault status | remaining_to_port 含 live 观察窗;shadow_observe=true | + +## 四、 预期结果 (Expected Results) + +- 所有默认路径 FAIL-CLOSED:不 Drain、不 Stage、不 Publish、不 claim fav lease +- shadow 不是写回执 +- live staging→artifact→publication **NOT OPENED** + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### 5.1 S1–S4 fav subscriber + +- **测试输入 (Input)**:`go test -count=1 ./internal/app/fav -run TestFavSubscriber -v` +- **实际输出 (Actual Output)**:三用例全部 PASS。见 `assets/2026-09-17-w6-fav-subscriber-shadow/unit-tests.txt` +- **评价判定 (Evaluation / Verdict)**:PASS +- **性能与耗时 (Duration & Latency)**:0.002s + +### 5.2 S5 vault status + +- **测试输入 (Input)**:`go test ./internal/server -run 'TestVaultDefaults|TestVaultStatus'` +- **实际输出 (Actual Output)**:`consumer_mode=off`、`shadow_observe=true`、`would_stage=false`、`would_publish=false`、`live_not_opened=true` +- **评价判定 (Evaluation / Verdict)**:PASS +- **性能与耗时 (Duration & Latency)**:0.007s + +### 5.3 S6 CLI stub + +- **测试输入 (Input)**:`go build ./cmd/artifact-vault && artifact-vault status` +- **实际输出 (Actual Output)**:见 `artifact-vault-status.json`。`consumer_mode=off`、`degraded_read=false`、`shadow_observe=true`、`would_stage=false`、`would_publish=false`;`remaining_to_port` 含 durable Register 与 live 三级观察窗。 +- **评价判定 (Evaluation / Verdict)**:PASS +- **性能与耗时 (Duration & Latency)**:<1s + +### 5.4 冒烟 + +- **测试输入 (Input)**:`go vet ./...`;`unset TEST_DATABASE_URL; go test -short ./...`;`./scripts/arch-check.sh --mode block` +- **实际输出 (Actual Output)**:`VET_EXIT=0`、`TEST_EXIT=0`、`arch-check block: production gate passed`。见 `smoke.txt` +- **评价判定 (Evaluation / Verdict)**:PASS +- **性能与耗时 (Duration & Latency)**:约 14s(short tests;`internal/server` 10.993s) + +### 5.5 live 放量 + +- **测试输入 (Input)**:compose 默认 `FAV_DEGRADED_READ` / `FAV_PUBLISHER_ENABLED` +- **实际输出 (Actual Output)**:docker-compose.dev.yml 仍 `${FAV_DEGRADED_READ:-false}` / `${FAV_PUBLISHER_ENABLED:-false}`;未改仓库默认;未开 staging Drain。 +- **评价判定 (Evaluation / Verdict)**:PASS(**live 未放量**,符合本波边界) +- **性能与耗时 (Duration & Latency)**:N/A + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +```text +ok internal/app/fav TestFavSubscriber* PASS +ok internal/server TestVaultDefaultsStayDisabled / TestVaultStatusReportsOff PASS +go test -short ./... TEST_EXIT=0 +go vet ./... VET_EXIT=0 +arch-check --mode block production gate passed +``` + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +W6 **shadow 切片 PASS**,可合 `dev`。FEATURE-MATRIX FAV **保持 partial**:live staging→artifact→publication 未开,观察窗 ≥7 天未开始计时。禁止整支 merge FAV;`FAV_DEGRADED_READ` 保持 false。durable `Register(fav)` 仍须单独 cutover 审批。 diff --git a/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/artifact-vault-status.json b/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/artifact-vault-status.json new file mode 100644 index 00000000..711ff426 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/artifact-vault-status.json @@ -0,0 +1,21 @@ +{ + "binary": "artifact-vault", + "consumer": "fav", + "consumer_mode": "off", + "degraded_read": false, + "note": "do not enable FAV_DEGRADED_READ or fav consumer without approval; subscriber shadow is read-only", + "publisher_default": false, + "remaining_to_port": [ + "enumerate.go (EnumerateTerminals / LoadExclusions)", + "TerminalRow + Backfill(rows) API (diverged from dataset Candidate path)", + "full recover-plan / recover-apply / gc-preflight / destroy", + "server ingest hooks (OnFinalArtifact*)", + "durable fav event Register after separate approval", + "live staging → artifact → publication (each ≥7d observation)" + ], + "shadow_observe": true, + "slice": "u7-incremental-stub", + "source_branch": "feature/final-artifact-vault", + "would_publish": false, + "would_stage": false +} diff --git a/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/smoke.txt b/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/smoke.txt new file mode 100644 index 00000000..ac8b8b70 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/smoke.txt @@ -0,0 +1,75 @@ +=== go vet === +VET_EXIT:0 +=== go test -short === +? chat-agent-system/cmd/artifact-vault [no test files] +ok chat-agent-system/cmd/bizdev-cli 2.318s +? chat-agent-system/cmd/dataset [no test files] +ok chat-agent-system/cmd/document-content-reconcile (cached) +? chat-agent-system/cmd/document-revision-backfill [no test files] +? chat-agent-system/cmd/eventmode [no test files] +? chat-agent-system/cmd/events [no test files] +? chat-agent-system/cmd/recategorize [no test files] +? chat-agent-system/cmd/server [no test files] +ok chat-agent-system/internal/agent 3.521s +ok chat-agent-system/internal/app 0.017s +ok chat-agent-system/internal/app/chatflow (cached) +ok chat-agent-system/internal/app/docadvance 0.002s +ok chat-agent-system/internal/app/docflow (cached) +ok chat-agent-system/internal/app/docnotify (cached) +ok chat-agent-system/internal/app/docproject (cached) +ok chat-agent-system/internal/app/fav 0.002s +ok chat-agent-system/internal/app/intentflow (cached) +ok chat-agent-system/internal/app/stageflow (cached) +ok chat-agent-system/internal/app/uow (cached) +ok chat-agent-system/internal/archtest 0.156s +ok chat-agent-system/internal/artifactvault (cached) +ok chat-agent-system/internal/auth (cached) +ok chat-agent-system/internal/authz (cached) +ok chat-agent-system/internal/bizdev 0.147s +ok chat-agent-system/internal/chat 0.012s +ok chat-agent-system/internal/config (cached) +ok chat-agent-system/internal/coordinator (cached) +ok chat-agent-system/internal/ctxmgr (cached) +ok chat-agent-system/internal/dataset (cached) +ok chat-agent-system/internal/db (cached) +ok chat-agent-system/internal/doccontent (cached) +ok chat-agent-system/internal/docduty (cached) +ok chat-agent-system/internal/docgov (cached) +ok chat-agent-system/internal/docquery (cached) +ok chat-agent-system/internal/docrevision (cached) +ok chat-agent-system/internal/events (cached) +ok chat-agent-system/internal/freedoc (cached) +ok chat-agent-system/internal/fsutil (cached) +ok chat-agent-system/internal/httpx (cached) +ok chat-agent-system/internal/intent (cached) +ok chat-agent-system/internal/kb (cached) +ok chat-agent-system/internal/maturity (cached) +ok chat-agent-system/internal/notification (cached) +? chat-agent-system/internal/observability [no test files] +ok chat-agent-system/internal/projectcontext (cached) +ok chat-agent-system/internal/projectinstance (cached) +ok chat-agent-system/internal/projectworkflow (cached) +ok chat-agent-system/internal/quote (cached) +ok chat-agent-system/internal/replyexport (cached) +ok chat-agent-system/internal/server 10.993s +ok chat-agent-system/internal/squad (cached) +ok chat-agent-system/internal/storage (cached) +ok chat-agent-system/internal/stream (cached) +ok chat-agent-system/internal/stream/transport (cached) +ok chat-agent-system/internal/swboot (cached) +ok chat-agent-system/internal/taskrun 0.194s +ok chat-agent-system/internal/template 0.999s +ok chat-agent-system/internal/upload (cached) +? chat-agent-system/internal/workflow [no test files] +ok chat-agent-system/internal/workflow/runtime (cached) +ok chat-agent-system/internal/workflowdef (cached) +ok chat-agent-system/internal/workspace (cached) +? chat-agent-system/templates [no test files] +TEST_EXIT:0 +=== arch-check === +arch-check mode=block +boundaries=/home/weichen/projects/Agent-System/.doc/system-design/module-boundaries.yaml +ok chat-agent-system/internal/archtest 0.133s +ok chat-agent-system/internal/archtest 0.144s +arch-check block: production gate passed +ARCH_EXIT:0 diff --git a/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/unit-tests.txt b/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/unit-tests.txt new file mode 100644 index 00000000..1cae73a5 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-17-w6-fav-subscriber-shadow/unit-tests.txt @@ -0,0 +1,15 @@ +=== RUN TestFavSubscriberDefaultsDisabled +--- PASS: TestFavSubscriberDefaultsDisabled (0.00s) +=== RUN TestFavSubscriberStillDisabledWhenFlagOnWithoutRegistryApproval +--- PASS: TestFavSubscriberStillDisabledWhenFlagOnWithoutRegistryApproval (0.00s) +=== RUN TestFavSubscriberShadowComputesIntendedOutcomeWithoutWrites +--- PASS: TestFavSubscriberShadowComputesIntendedOutcomeWithoutWrites (0.00s) +PASS +ok chat-agent-system/internal/app/fav 0.002s +=== RUN TestVaultDefaultsStayDisabled +--- PASS: TestVaultDefaultsStayDisabled (0.00s) +=== RUN TestVaultStatusReportsOff +--- PASS: TestVaultStatusReportsOff (0.00s) +PASS +ok chat-agent-system/internal/server 0.007s +EXIT:0 diff --git a/cmd/artifact-vault/main.go b/cmd/artifact-vault/main.go index 771a992d..63ce2467 100644 --- a/cmd/artifact-vault/main.go +++ b/cmd/artifact-vault/main.go @@ -32,9 +32,13 @@ func main() { "full recover-plan / recover-apply / gc-preflight / destroy", "server ingest hooks (OnFinalArtifact*)", "durable fav event Register after separate approval", + "live staging → artifact → publication (each ≥7d observation)", }, - "source_branch": "feature/final-artifact-vault", - "note": "do not enable FAV_DEGRADED_READ or fav consumer without approval", + "shadow_observe": true, + "would_stage": false, + "would_publish": false, + "source_branch": "feature/final-artifact-vault", + "note": "do not enable FAV_DEGRADED_READ or fav consumer without approval; subscriber shadow is read-only", }) case "help", "-h", "--help": usage() diff --git a/internal/app/fav/subscriber.go b/internal/app/fav/subscriber.go index 84c38715..1552c851 100644 --- a/internal/app/fav/subscriber.go +++ b/internal/app/fav/subscriber.go @@ -8,6 +8,8 @@ package fav import ( "context" "fmt" + "os" + "strings" "chat-agent-system/internal/artifactvault" "chat-agent-system/internal/events" @@ -24,6 +26,8 @@ type EventConsumer struct { // Enabled must stay false by default. Even when true, events.Registry // still rejects Register(ConsumerFAV) until the hard gate is lifted. Enabled bool + // AllowDegradedRead must stay false. Shadow refuses if this is true. + AllowDegradedRead bool } // HandleLease refuses work while the subscriber is disabled (default). @@ -44,3 +48,46 @@ func (c *EventConsumer) HandleLease(ctx context.Context, lease events.Lease, tx func DefaultEnabled() bool { return false } + +// IntendedOutcome is a read-only shadow calculation. It must never be treated +// as a write receipt; WouldStage/WouldPublish stay false in this slice. +type IntendedOutcome struct { + EventName string + AggregateType string + AggregateID string + LogicalResource string + WouldStage bool + WouldPublish bool + DegradedReadUsed bool +} + +// ObserveShadow computes the intended FAV outcome without business side effects. +// FAV_DEGRADED_READ must stay false; live staging/artifact/publication is not opened. +func (c *EventConsumer) ObserveShadow(lease events.Lease) (IntendedOutcome, error) { + out := IntendedOutcome{ + EventName: lease.EventName, + AggregateType: lease.AggregateType, + AggregateID: lease.AggregateID, + LogicalResource: strings.TrimSpace(lease.AggregateType) + ":" + strings.TrimSpace(lease.AggregateID), + } + if c == nil { + return out, fmt.Errorf("%w: fav", events.ErrDisabledConsumer) + } + if c.AllowDegradedRead || envTruthy("FAV_DEGRADED_READ") { + return out, fmt.Errorf("fav shadow refuses FAV_DEGRADED_READ") + } + out.WouldStage = false + out.WouldPublish = false + out.DegradedReadUsed = false + _ = c.Publisher // never Drain / Stage / Publish here + return out, nil +} + +func envTruthy(key string) bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} diff --git a/internal/app/fav/subscriber_test.go b/internal/app/fav/subscriber_test.go index a08485ca..69d35be9 100644 --- a/internal/app/fav/subscriber_test.go +++ b/internal/app/fav/subscriber_test.go @@ -33,3 +33,36 @@ func TestFavSubscriberStillDisabledWhenFlagOnWithoutRegistryApproval(t *testing. t.Fatal("Registry.Register(fav) must remain fail-closed") } } + +func TestFavSubscriberShadowComputesIntendedOutcomeWithoutWrites(t *testing.T) { + t.Setenv("FAV_DEGRADED_READ", "false") + t.Setenv("FAV_PUBLISHER_ENABLED", "false") + c := &fav.EventConsumer{Enabled: false, Env: "dev"} + out, err := c.ObserveShadow(events.Lease{ + EventName: "document.finalized", AggregateType: "document", AggregateID: "doc_1", + }) + if err != nil { + t.Fatal(err) + } + if out.WouldStage || out.WouldPublish || out.DegradedReadUsed { + t.Fatalf("shadow must not claim writes: %+v", out) + } + if out.LogicalResource != "document:doc_1" { + t.Fatalf("logical=%s", out.LogicalResource) + } + err = c.HandleLease(context.Background(), events.Lease{}, nil) + if !errors.Is(err, events.ErrDisabledConsumer) { + t.Fatalf("HandleLease still disabled: %v", err) + } + + c.AllowDegradedRead = true + if _, err := c.ObserveShadow(events.Lease{}); err == nil { + t.Fatal("shadow must refuse degraded read") + } + + c.AllowDegradedRead = false + t.Setenv("FAV_DEGRADED_READ", "true") + if _, err := c.ObserveShadow(events.Lease{AggregateType: "document", AggregateID: "doc_2"}); err == nil { + t.Fatal("shadow must refuse process env FAV_DEGRADED_READ") + } +} diff --git a/internal/server/vault.go b/internal/server/vault.go index ef015028..d9c1d3ee 100644 --- a/internal/server/vault.go +++ b/internal/server/vault.go @@ -95,7 +95,11 @@ func (s *Server) handleVaultStatus(w http.ResponseWriter, r *http.Request) { "degraded_read": degraded, "store_configured": storeReady, "subscriber_default": false, - "note": "fav consumer remains OFF until separate approval; full FAV branch not merged", + "shadow_observe": true, + "would_stage": false, + "would_publish": false, + "live_not_opened": true, + "note": "fav consumer remains OFF until separate approval; shadow computes intended outcome only; full FAV branch not merged", }) } diff --git a/internal/server/vault_test.go b/internal/server/vault_test.go index c6b7afe2..de02ed4d 100644 --- a/internal/server/vault_test.go +++ b/internal/server/vault_test.go @@ -57,6 +57,15 @@ func TestVaultStatusReportsOff(t *testing.T) { if body["degraded_read"] != false { t.Fatalf("degraded_read=%v", body["degraded_read"]) } + if body["shadow_observe"] != true { + t.Fatalf("shadow_observe=%v", body["shadow_observe"]) + } + if body["would_stage"] != false || body["would_publish"] != false { + t.Fatalf("shadow must not claim writes: %+v", body) + } + if body["live_not_opened"] != true { + t.Fatalf("live_not_opened=%v", body["live_not_opened"]) + } } func TestValidateIndependentVaultRejectsDataRoot(t *testing.T) {