merge: feature/multi-intent-u1-contracts

This commit is contained in:
weichen
2026-09-15 16:27:29 +00:00
21 changed files with 945 additions and 21 deletions
+4 -2
View File
@@ -5,7 +5,7 @@
## 焦点
1. **MULTI-INTENT HITL**:方案已复核,U1 契约未开工([设计](../system-design/2026-09-15-multi-intent-hitl-plan.md);[short-term](./short-term/2026-09-15-multi-intent-hitl.md))
1. **MULTI-INTENT HITL**:U1 契约已冻结([U1 报告](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md);[U1 任务](./short-term/2026-09-15-multi-intent-u1-contracts.md));U2 规则分段未开工
2. **发布门禁**:CI `TEST_DATABASE_URL`、真实 PI 三工作流 E2E、前端人工抽检仍开放([gap-matrix](../remediation/gap-matrix.md))
3. **CHAT-INTENT**:双标签 busy 已签(项目 **6275**);不删 `sess-p143-u9`;项目 143 只读
@@ -13,6 +13,7 @@
| owner | task | branch | worktree | mutex | status |
|-------|------|--------|----------|-------|--------|
| Cursor | 2026-09-15-multi-intent-u1-contracts | feature/multi-intent-u1-contracts | - | architecture-docs | done |
| Cursor | app-composition-u11-u12 | feature/app-composition-u11-u12 | - | app | done |
| Cursor | chatlog-contextasm-u10 | feature/chatlog-contextasm-u10 | - | chat | done |
| Cursor | chat-history-refresh-remediation | fix/chat-history-refresh | - | chat | done |
@@ -84,7 +85,7 @@
| holder | phase | since | scope | queue |
|--------|-------|-------|-------|-------|
| - | idle | - | - | - |
| Cursor | merging | 2026-09-15 | multi-intent-u1-contracts | - |
**phase**: `idle` | `merging` | `smoke` | `e2e`
@@ -118,6 +119,7 @@
| Phase 2 方案审阅 | [../audit/2026-09-15-module-decoupling-phase2-plan-review.md](../audit/2026-09-15-module-decoupling-phase2-plan-review.md) |
| Phase 2 修订计划 | [../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md](../remediation/2026-09-15-module-decoupling-ipc-phase2-remediation-plan.md) |
| 多意图 HITL 复核方案 | [../system-design/2026-09-15-multi-intent-hitl-plan.md](../system-design/2026-09-15-multi-intent-hitl-plan.md) |
| 多意图 HITL U1 契约 | [../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md) |
| 文档可靠性 M1 报告 | [../test-reports/2026-09-14-document-reliability-pilot-test-report.md](../test-reports/2026-09-14-document-reliability-pilot-test-report.md) |
| 文档可靠性 M1 活验 | [../test-reports/2026-09-14-document-reliability-pilot-live-test-report.md](../test-reports/2026-09-14-document-reliability-pilot-live-test-report.md) |
| 文档可靠性 M2 | [../test-reports/2026-09-14-document-reliability-m2-test-report.md](../test-reports/2026-09-14-document-reliability-m2-test-report.md) |
@@ -0,0 +1,22 @@
---
title: "多意图 HITL U1 契约冻结 checklist"
status: done
created: 2026-09-15
updated: 2026-09-15
related_task: "2026-09-15-multi-intent-u1-contracts"
---
# 多意图 HITL U1 契约冻结 checklist
## Wave 0
- [x] YAML `app/intentflow` allowlist + 三表 owner + unwrap_owners — 证据:`.doc/system-design/module-boundaries.yaml`
- [x] 三条 `intent.plan.*` YAML ↔ StaticContracts parity — 证据:`go test ./internal/events -run RegistryParity`
- [x] archtest 扫描 `intent.plan.*` — 证据:`go test ./internal/archtest`
- [x] intentflow 状态/错误码/Service/政策/legacy tier — 证据:`go test ./internal/app/intentflow`
- [x] CSRF/Bearer 结论落盘 — 证据:U1 short-term
- [x] `go test -short ./...` — 证据:[U1 报告](../../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)
## 完成定义
- [x] 全部勾选且有证据链接
@@ -1,12 +1,12 @@
---
title: "多意图持久计划与 HITL(核心 U1–U8,扩面 U9–U12)"
status: planned
status: in_progress
created: 2026-09-15
updated: 2026-09-15
owner: "-(待逐单元认领)"
branch: feature/multi-intent-u1-contracts
owner: "-(U1 已完成;U2 待认领)"
branch: "-"
worktree: "-"
mutex: architecture-docs
mutex: "-"
---
# 多意图持久计划与 HITL(核心 U1–U8,扩面 U9–U12)
@@ -51,7 +51,7 @@ mutex: architecture-docs
## 步骤
1. [ ] U1 冻结边界、状态机、批量策略、错误码、事件和默认 legacy tier;
1. [x] U1 冻结边界、状态机、批量策略、错误码、事件和默认 legacy tier([独立任务](./2026-09-15-multi-intent-u1-contracts.md);[报告](../../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md));
2. [ ] U2 characterization-first 建 golden/hard-negative 与 shadow 基线;
3. [ ] U3 建 intentflow owner、PG schema、CAS/lease/fencing/retention;
4. [ ] U4 接 Track E outbox/dispatcher,只投影状态事实;
@@ -92,12 +92,12 @@ mutex: architecture-docs
## 相关链接
- Checklist: U1 实施时建立逐波 checklist
- Checklist: [U1](../checklists/2026-09-15-multi-intent-u1-contracts.md)
- Handoff: 每个 U-ID 独立建立
- Test report: 每个 U-ID 独立归档 `.doc/test-reports/`
- Test report: [U1](../../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)
## 证据 / 备注
- 当前 `plan_builder.go` 最多两段且写意图返回 nil;`action_planner.go` 的确认仍是文字提示。
- 当前 `module-boundaries.yaml` 中 intent 仅依赖 maturity;Track E live,FAV off。
- 本次只修订方案与任务文档,不实施、不提交、不推送。
- U1 已冻结 intentflow 契约、事件与 BatchActionPolicy,默认 `MULTI_INTENT_PLAN_TIER=legacy`。
@@ -0,0 +1,76 @@
---
title: "多意图 HITL U1:契约冻结"
status: done
created: 2026-09-15
updated: 2026-09-15
owner: Cursor
branch: feature/multi-intent-u1-contracts
worktree: "-"
mutex: architecture-docs
---
# 多意图 HITL U1:契约冻结
## 目标
冻结 `app/intentflow` 边界、状态机、BatchActionPolicy、错误码、事件契约与默认 `legacy` tier;不改识别路由、不建表、不接 HTTP。
## 范围
- `module-boundaries.yaml`:`app/intentflow` allowlist、三表 owner、`unwrap_owners`、三条 `intent.plan.*` 事件
- `events.StaticContracts` 与 YAML 对等;archtest 扫描 `intent.plan.*`
- `internal/app/intentflow` 骨架:状态、错误码、Service 端口、政策、tier 开关
- CSRF/认证结论:现有 Bearer + CORS 对 U5 足够,不是 blocker
## 非目标
- 不修改 `internal/intent` 业务类型或 `MULTI_INTENT_ENABLED`
- 不建 Goose 表、不接线 `app.Assemble`、不改 chat/coordinator/Web
- 不 Append 事件、不实施 U2–U12
## REST(U5 再挂路由)
- `GET /api/v1/intent-plans/active`
- `GET /api/v1/intent-plans/{plan_id}`
- `POST /api/v1/intent-plans/{plan_id}/decisions`
- body:`expected_revision`、`decision_nonce`、`decision_digest`、`kind`、步骤选择
## CSRF / 认证(U5 前置结论,非 blocker)
现有认证是 `Authorization: Bearer` 或 `X-Session-Token` 请求头(`internal/server/auth_handlers.go`),无 cookie 会话,无独立 CSRF 中间件;另有 Origin allowlist CORS。
- 自定义头不会被浏览器跨站表单自动带上,现有模式对 U5 足够
- `decision_nonce` / 幂等键只防重放与并发,**不替代** CSRF 或身份认证
- U5 必须 `requireAuthenticatedUser` + 四元作用域(environment/project/session/user)
- **禁止**为计划 API 改成 cookie 自动提交
- 若未来改 cookie 认证,另立 auth 任务,不得在 intent-plan 端点发明弱 CSRF
## 步骤
1. [x] 从最新 `dev` 检出 `feature/multi-intent-u1-contracts` 并认领 CURRENT
2. [x] 登记边界 YAML、事件契约、archtest regex
3. [x] 落地 intentflow 骨架与 BatchActionPolicy
4. [x] arch/parity/short 测试与 U1 报告
## 验收标准
- [x] `intent -> maturity` 保持;server/chat 不直写计划表
- [x] YAML ↔ StaticContracts parity 绿
- [x] 未知 action/policy key fail closed;ActionRegistry 无 batch/tier 字段
- [x] 默认 `MULTI_INTENT_PLAN_TIER=legacy`
- [x] 单意图生成策略不变(本单元无运行时改动)
## 相关链接
- 总览: [2026-09-15-multi-intent-hitl.md](./2026-09-15-multi-intent-hitl.md)
- 设计: [../../system-design/2026-09-15-multi-intent-hitl-plan.md](../../system-design/2026-09-15-multi-intent-hitl-plan.md)
- Checklist: [../checklists/2026-09-15-multi-intent-u1-contracts.md](../checklists/2026-09-15-multi-intent-u1-contracts.md)
- Test report: [../../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md](../../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md)
## 证据 / 备注
- 事件登记窗口同时占用 `events` mutex;U1 不改 intent 代码。
- 生产者含 `events`:StaticContracts 字面量落在 `internal/events/registry.go`,与现有 document.* 先例一致。
- CSRF:Bearer / `X-Session-Token` + CORS allowlist,U5 非 blocker。
- 冒烟:`go vet ./...`、`go test -short ./...`、`./scripts/arch-check.sh --mode block` PASS。U1 无 schema/容器变更,集成槽位保持 idle。
- 提交:`bdccb4d` docs(ops);`aefb1e4` feat(events);`9364da4` feat(intentflow);随后 docs(status)。
+2 -1
View File
@@ -15,7 +15,8 @@
| 模块 | 能力 | 状态 | 证据 | 缺口/后续 | mutex |
|------|------|------|------|-----------|-------|
| 意图识别 L1 | 18 类 Intent + 三分支消解 | partial | [system-design/intent-recognition](../system-design/intent-recognition/);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[运行态报告](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);[双标签活验](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json);[多意图复核方案](../system-design/2026-09-15-multi-intent-hitl-plan.md) | 明确写动作与只读总结已分流;双标签 `DOC_BUSY` 已签(项目 6275)。多意图持久计划仅完成方案复核,核心 U1–U8 与扩面 U9–U12 均未实施;intent 纯依赖边界不得回退 | `intent` |
| 意图识别 L1 | 18 类 Intent + 三分支消解 | partial | [system-design/intent-recognition](../system-design/intent-recognition/);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[运行态报告](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);[双标签活验](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json);[多意图复核方案](../system-design/2026-09-15-multi-intent-hitl-plan.md);[U1 契约](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md) | 明确写动作与只读总结已分流;双标签 `DOC_BUSY` 已签(项目 6275)。多意图 U1 契约已冻(intentflow 边界/事件/BatchActionPolicy/legacy tier);U2–U8 未实施;intent 纯依赖边界不得回退 | `intent` |
| 多意图持久计划 | app/intentflow 生命周期与 HITL 契约 | planned | [设计](../system-design/2026-09-15-multi-intent-hitl-plan.md);[U1 报告](../test-reports/2026-09-15-multi-intent-u1-contracts-test-report.md);[U1 任务](../ops/short-term/2026-09-15-multi-intent-u1-contracts.md) | U1 仅冻结合约。U2 规则分段 shadow;U3 PG/CAS;U4 投影;U5–U8 决策/Web/只读执行/task.generate | `intent-plan` |
| 对话 / ctxmgr | 多轮上下文、记忆、摘要 | partial | [system-design/multi-turn-dialogue](../system-design/multi-turn-dialogue/);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md);[U10 端口化](../archive/ops/2026-09-15-chatlog-contextasm-u10.md);[U10 报告](../test-reports/2026-09-15-chatlog-contextasm-u10-test-report.md) | `CHAT-HISTORY-REFRESH` 闲聊刷新活验已补。U10 原地端口 **停止扩面**(未物理拆包)。语义摘要(D5 backlog) | `chat` |
| 智能体小队 | 角色-任务-工作区-黑板 | done | [system-design/agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6 backlog) | `squad` |
| 工作流治理 | 三正式工作流 + 阶段门禁 | partial | [remediation/three-workflows](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[test 2026-09-12](../test-reports/2026-09-12-three-workflows-governance-remediation-test-report.md);[冻结态/职责可选整改](../remediation/2026-09-14-workflow-plan-409-frozen-and-optional-duties-remediation.md);[Wave 5 页面矩阵](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[职责-动作鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md) | 设置页/DutyMode + 动作级鉴权/未激活 202-blocked/关闭创建直通已落地。后续:none 文档真实 PI 审批长链路、CI 三工作流 E2E | `workflow` |
+1
View File
@@ -126,6 +126,7 @@
| 主方案 | [2026-09-15-multi-intent-hitl-plan.md](2026-09-15-multi-intent-hitl-plan.md) |
| 复核 | [../audit/2026-09-15-multi-intent-hitl-plan-review.md](../audit/2026-09-15-multi-intent-hitl-plan-review.md) |
| 任务总览 | [../ops/short-term/2026-09-15-multi-intent-hitl.md](../ops/short-term/2026-09-15-multi-intent-hitl.md) |
| U1 契约 | [../ops/short-term/2026-09-15-multi-intent-u1-contracts.md](../ops/short-term/2026-09-15-multi-intent-u1-contracts.md) |
| 一句话总结 | intent 保持纯规则,intentflow 持久化计划并统一确认、授权、幂等、恢复和逐动作执行 |
**硬边界**:确认不等于授权;PG 是计划真相,卡片/SSE 是投影;写动作按 owner 端口逐项接入;LLM 只能提出不可信分段建议。
+30
View File
@@ -65,6 +65,7 @@ imports:
- app/docflow
- app/docnotify
- app/docproject
- app/intentflow
- app/stageflow
- app/uow
- artifactvault
@@ -113,6 +114,10 @@ imports:
- docquery
- docrevision
- events
app/intentflow:
- app/uow
- events
- intent
app/uow: []
app/stageflow:
- bizdev
@@ -304,6 +309,12 @@ table_owners:
- dataset
dataset_destroy_requests:
- dataset
intent_plans:
- app/intentflow
intent_plan_steps:
- app/intentflow
intent_plan_decisions:
- app/intentflow
path_owners:
output.md:
@@ -348,6 +359,15 @@ event_producers:
- taskrun
fav.artifact_staged:
- app/docflow
intent.plan.awaiting_input:
- app/intentflow
- events
intent.plan.step_changed:
- app/intentflow
- events
intent.plan.completed:
- app/intentflow
- events
event_consumers:
outbox.notify:
@@ -371,6 +391,15 @@ event_consumers:
- notify
fav.artifact_staged:
- fav
intent.plan.awaiting_input:
- projection
- notify
intent.plan.step_changed:
- projection
- notify
intent.plan.completed:
- projection
- notify
unwrap_owners:
- docgov
@@ -380,6 +409,7 @@ unwrap_owners:
- events
- artifactvault
- ctxmgr
- app/intentflow
exceptions:
- kind: sql_write
@@ -0,0 +1,105 @@
# 多意图 HITL U1 契约冻结 测试报告
## 一、 测试背景 (Background)
多意图识别目前最多拆两段且写意图直接丢弃;确认只是 coordinator 文本提示。方案已复核为「持久计划深模块」,U1 必须先冻结边界、事件、状态机和 BatchActionPolicy,避免后续单元向 `intent` / coordinator 堆叠职责。
## 二、 测试目标 (Test Objectives)
- `app/intentflow` 进入 import allowlist;三张计划表 owner 仅为 `app/intentflow`
- 三条 `intent.plan.*` 事实事件 YAML ↔ `StaticContracts` 对等,payload 不含 prompt/slots/权限
- 默认 `MULTI_INTENT_PLAN_TIER=legacy`;未知 policy key fail closed
- ActionRegistry 不出现 batch/tier/adapter 字段
- `intent -> maturity` 与全仓 short 测试无回归
- 无运行时路由变更,因此不做真实 PI / 浏览器验收
## 三、 测试方案与环境 (Test Plan & Environment)
| 项 | 值 |
|----|-----|
| OS | Linux 7.0.0-31-generic |
| Go | go1.27.1 linux/amd64 |
| 分支 | `feature/multi-intent-u1-contracts`(从本地 `dev@9874237` 检出) |
| Schema | **43**(本单元不迁移) |
| 容器 | 不 rebuild |
| 项目 143 | **只读** |
| 证据 | `.doc/test-reports/assets/2026-09-15-multi-intent-u1-contracts/` |
测试层级:archtest / events parity / intentflow 单测 / `go test -short ./...`。无 PG 集成、无真实 PI。
## 四、 预期结果 (Expected Results)
| 用例 | 判定 |
|------|------|
| YAML ↔ StaticContracts | 三条 `intent.plan.*` 生产者含 `app/intentflow`+`events`,消费者 `projection`+`notify` |
| 未知 action key | `Lookup` 失败;构造期缺行/多行失败 |
| `task.run` | 批量必须确认,`OpenFromTier=confirm` |
| `regulation.query` | 不要求批量确认,`OpenFromTier=auto_read` |
| 默认 tier | `legacy`;`MULTI_INTENT_ENABLED=true` 不升档 |
| ActionSpec | 无 batch/tier/adapter 字段 |
| arch-check block | production gate passed |
| 全仓 short | PASS |
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
### 5.1 事件 parity 与安全 payload
- **测试输入 (Input)**:`go test -count=1 ./internal/events -run 'RegistryParity|IntentPlan'`
- **实际输出 (Actual Output)**:
```
ok chat-agent-system/internal/events 0.004s
```
(完整包输出见 [unit.txt](assets/2026-09-15-multi-intent-u1-contracts/unit.txt))
- **评价判定 (Evaluation / Verdict)**:PASS。`TestRegistryParityWithYAML`、`TestIntentPlanContractsOmitSensitivePayload` 绿。
- **性能与耗时 (Duration & Latency)**:约 5ms
### 5.2 archtest / arch-check
- **测试输入**:`./scripts/arch-check.sh --mode block`;`go test -count=1 ./internal/archtest`
- **实际输出**:
```
arch-check mode=block
ok chat-agent-system/internal/archtest 0.173s
ok chat-agent-system/internal/archtest 0.154s
arch-check block: production gate passed
```
- **评价判定**:PASS。`intent.plan.*` 字面量可被扫描;`events` 已登记为生产者,registry.go 不误报。
- **性能与耗时**:约 1.1s
### 5.3 intentflow 政策与 tier
- **测试输入**:`go test -count=1 ./internal/app/intentflow`
- **实际输出**:`ok chat-agent-system/internal/app/intentflow 0.002s`
- **评价判定**:PASS。未知 key fail closed;`task.run` 批量确认;查询不要求确认;ActionSpec 无 HITL 字段;默认 legacy。
- **性能与耗时**:约 2ms
### 5.4 全仓 short 回归
- **测试输入**:`unset TEST_DATABASE_URL; go test -short -count=1 ./...`;`go vet ./...`;`git diff --check`
- **实际输出**:见 [short.txt](assets/2026-09-15-multi-intent-u1-contracts/short.txt)。`internal/server` 9.920s;全包 ok。`go vet` 无输出。`git diff --check` 无空白错误。
- **评价判定**:PASS。单意图路径无运行时改动,`internal/intent` / `internal/coordinator` / `internal/chat` 均绿。
- **性能与耗时**:约 12s
## 六、 自动化测试套件汇总 (Test Suite Execution)
| 命令 | 结果 |
|------|------|
| `go vet ./...` | PASS |
| `go test -short -count=1 ./internal/app/intentflow ./internal/events ./internal/archtest ./internal/intent` | PASS |
| `go test -short -count=1 ./...` | PASS |
| `./scripts/arch-check.sh --mode block` | PASS |
通过率 100%。未跑 PG / 真实 PI(本单元无副作用路径)。
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
U1 达到合入标准:契约冻结完成,默认 legacy,确认前零副作用保持不变。
后续:U2 在独立分支 `feature/multi-intent-u2-segmenter` 做规则分段 shadow;不得把 HITL/tier 回灌 ActionRegistry。
CSRF:现有 Bearer / `X-Session-Token` + CORS allowlist 对 U5 足够,不是 blocker。nonce 不替代认证。
@@ -0,0 +1,61 @@
ok chat-agent-system/cmd/bizdev-cli 2.374s
? chat-agent-system/cmd/dataset [no test files]
ok chat-agent-system/cmd/document-content-reconcile 0.003s
? 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.653s
ok chat-agent-system/internal/app 0.016s
ok chat-agent-system/internal/app/chatflow 0.005s
ok chat-agent-system/internal/app/docadvance 0.003s
ok chat-agent-system/internal/app/docflow 0.004s
ok chat-agent-system/internal/app/docnotify 0.004s
ok chat-agent-system/internal/app/docproject 0.004s
ok chat-agent-system/internal/app/intentflow 0.002s
ok chat-agent-system/internal/app/stageflow 0.004s
ok chat-agent-system/internal/app/uow 0.004s
ok chat-agent-system/internal/archtest 0.172s
ok chat-agent-system/internal/artifactvault 0.004s
ok chat-agent-system/internal/auth 0.821s
ok chat-agent-system/internal/authz 0.004s
ok chat-agent-system/internal/bizdev 0.165s
ok chat-agent-system/internal/chat 0.014s
ok chat-agent-system/internal/config 0.002s
ok chat-agent-system/internal/coordinator 0.010s
ok chat-agent-system/internal/ctxmgr 0.083s
ok chat-agent-system/internal/dataset 0.003s
ok chat-agent-system/internal/db 0.003s
ok chat-agent-system/internal/doccontent 0.002s
ok chat-agent-system/internal/docduty 0.005s
ok chat-agent-system/internal/docgov 0.016s
ok chat-agent-system/internal/docquery 0.003s
ok chat-agent-system/internal/docrevision 0.007s
ok chat-agent-system/internal/events 0.005s
ok chat-agent-system/internal/freedoc 0.023s
ok chat-agent-system/internal/fsutil 0.002s
ok chat-agent-system/internal/httpx 0.003s
ok chat-agent-system/internal/intent 0.011s
ok chat-agent-system/internal/kb 0.005s
ok chat-agent-system/internal/maturity 0.002s
ok chat-agent-system/internal/notification 0.068s
? chat-agent-system/internal/observability [no test files]
ok chat-agent-system/internal/projectcontext 0.004s
ok chat-agent-system/internal/projectinstance 0.004s
ok chat-agent-system/internal/projectworkflow 0.003s
ok chat-agent-system/internal/quote 0.007s
ok chat-agent-system/internal/replyexport 0.002s
ok chat-agent-system/internal/server 9.920s
ok chat-agent-system/internal/squad 0.030s
ok chat-agent-system/internal/storage 0.016s
ok chat-agent-system/internal/stream 0.003s
ok chat-agent-system/internal/stream/transport 0.002s
ok chat-agent-system/internal/taskrun 0.142s
ok chat-agent-system/internal/template 0.785s
ok chat-agent-system/internal/upload 0.002s
? chat-agent-system/internal/workflow [no test files]
ok chat-agent-system/internal/workflow/runtime 0.003s
ok chat-agent-system/internal/workflowdef 0.003s
ok chat-agent-system/internal/workspace 0.003s
? chat-agent-system/templates [no test files]
@@ -0,0 +1,4 @@
ok chat-agent-system/internal/app/intentflow 0.002s
ok chat-agent-system/internal/events 0.004s
ok chat-agent-system/internal/archtest 0.161s
ok chat-agent-system/internal/intent 0.010s
+13
View File
@@ -0,0 +1,13 @@
// Package intentflow owns the multi-intent plan lifecycle.
//
// It is the only application entry for proposing, confirming, recovering, and
// advancing persistent plans. Callers (chat/server) must depend on the Service
// interface and PlanView projection, not on the store, pool, or action-owner
// engines.
//
// Hard rules:
// - Confirmation is not authorization; each step re-checks the current principal.
// - PostgreSQL plan/step/decision rows are authoritative; cards and SSE are projections.
// - internal/intent stays a pure rule module (intent -> maturity).
// - This package must not import docgov, taskrun, coordinator, server, or pgx.
package intentflow
+39
View File
@@ -0,0 +1,39 @@
package intentflow
import "errors"
// Sentinel errors use the public API error codes from the design §9.
var (
ErrPlanNotFoundOrHidden = errors.New("PLAN_NOT_FOUND_OR_HIDDEN")
ErrPlanStaleRevision = errors.New("PLAN_STALE_REVISION")
ErrPlanExpired = errors.New("PLAN_EXPIRED")
ErrPlanDecisionConflict = errors.New("PLAN_DECISION_CONFLICT")
ErrPlanDependencyInvalid = errors.New("PLAN_DEPENDENCY_INVALID")
ErrPlanActionForbidden = errors.New("PLAN_ACTION_FORBIDDEN")
ErrPlanActionVersionConflict = errors.New("PLAN_ACTION_VERSION_CONFLICT")
ErrUnauthenticated = errors.New("ERR_UNAUTHENTICATED")
)
// ErrorCode returns the stable machine code for a sentinel, or empty.
func ErrorCode(err error) string {
switch {
case errors.Is(err, ErrPlanNotFoundOrHidden):
return "PLAN_NOT_FOUND_OR_HIDDEN"
case errors.Is(err, ErrPlanStaleRevision):
return "PLAN_STALE_REVISION"
case errors.Is(err, ErrPlanExpired):
return "PLAN_EXPIRED"
case errors.Is(err, ErrPlanDecisionConflict):
return "PLAN_DECISION_CONFLICT"
case errors.Is(err, ErrPlanDependencyInvalid):
return "PLAN_DEPENDENCY_INVALID"
case errors.Is(err, ErrPlanActionForbidden):
return "PLAN_ACTION_FORBIDDEN"
case errors.Is(err, ErrPlanActionVersionConflict):
return "PLAN_ACTION_VERSION_CONFLICT"
case errors.Is(err, ErrUnauthenticated):
return "ERR_UNAUTHENTICATED"
default:
return ""
}
}
+89
View File
@@ -0,0 +1,89 @@
package intentflow
import (
"os"
"strings"
"sync"
)
// PlanTier is the intentflow rollout switch. It must not be stored on
// intent.ActionSpec or intent.FeatureFlags.
type PlanTier string
const (
TierLegacy PlanTier = "legacy"
TierShadow PlanTier = "shadow"
TierAutoRead PlanTier = "auto_read"
TierConfirm PlanTier = "confirm"
TierFull PlanTier = "full"
)
type FeatureFlags struct {
PlanTier PlanTier
}
var (
flagsMu sync.RWMutex
activeFlags = defaultFeatureFlags()
)
func defaultFeatureFlags() FeatureFlags {
return FeatureFlags{PlanTier: parsePlanTier(os.Getenv("MULTI_INTENT_PLAN_TIER"))}
}
func parsePlanTier(raw string) PlanTier {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", "legacy":
return TierLegacy
case "shadow":
return TierShadow
case "auto_read", "autoread":
return TierAutoRead
case "confirm":
return TierConfirm
case "full":
return TierFull
default:
return TierLegacy
}
}
func CurrentFeatureFlags() FeatureFlags {
flagsMu.RLock()
defer flagsMu.RUnlock()
return activeFlags
}
func SetFeatureFlagsForTest(f FeatureFlags) {
flagsMu.Lock()
activeFlags = f
flagsMu.Unlock()
}
func ResetFeatureFlagsForTest() {
flagsMu.Lock()
activeFlags = defaultFeatureFlags()
flagsMu.Unlock()
}
// Allows reports whether the current (or supplied) tier may activate a policy row.
func (t PlanTier) Allows(min PlanTier) bool {
return tierRank(t) >= tierRank(min)
}
func tierRank(t PlanTier) int {
switch t {
case TierLegacy:
return 0
case TierShadow:
return 1
case TierAutoRead:
return 2
case TierConfirm:
return 3
case TierFull:
return 4
default:
return 0
}
}
+82
View File
@@ -0,0 +1,82 @@
package intentflow
import (
"testing"
)
func TestDefaultPlanTierIsLegacy(t *testing.T) {
t.Setenv("MULTI_INTENT_PLAN_TIER", "")
ResetFeatureFlagsForTest()
if got := CurrentFeatureFlags().PlanTier; got != TierLegacy {
t.Fatalf("default tier = %s, want legacy", got)
}
}
func TestUnknownPlanTierFailsClosedToLegacy(t *testing.T) {
if parsePlanTier("definitely-not-a-tier") != TierLegacy {
t.Fatal("unknown MULTI_INTENT_PLAN_TIER must fail closed to legacy")
}
}
func TestPlanTierIndependentOfLegacyMultiIntentFlag(t *testing.T) {
t.Setenv("MULTI_INTENT_ENABLED", "true")
t.Setenv("MULTI_INTENT_PLAN_TIER", "")
ResetFeatureFlagsForTest()
if CurrentFeatureFlags().PlanTier != TierLegacy {
t.Fatal("MULTI_INTENT_ENABLED must not promote plan tier")
}
}
func TestPlanTierEnv(t *testing.T) {
if parsePlanTier("shadow") != TierShadow {
t.Fatal("shadow")
}
if parsePlanTier("auto_read") != TierAutoRead {
t.Fatal("auto_read")
}
if parsePlanTier("confirm") != TierConfirm {
t.Fatal("confirm")
}
if parsePlanTier("full") != TierFull {
t.Fatal("full")
}
if !TierConfirm.Allows(TierAutoRead) || TierShadow.Allows(TierConfirm) {
t.Fatal("tier rank")
}
}
func TestErrorCodes(t *testing.T) {
if ErrorCode(ErrPlanNotFoundOrHidden) != "PLAN_NOT_FOUND_OR_HIDDEN" {
t.Fatal(ErrorCode(ErrPlanNotFoundOrHidden))
}
if ErrorCode(ErrPlanStaleRevision) != "PLAN_STALE_REVISION" {
t.Fatal("stale")
}
if ErrorCode(ErrPlanExpired) != "PLAN_EXPIRED" {
t.Fatal("expired")
}
if ErrorCode(ErrPlanDecisionConflict) != "PLAN_DECISION_CONFLICT" {
t.Fatal("conflict")
}
if ErrorCode(ErrPlanDependencyInvalid) != "PLAN_DEPENDENCY_INVALID" {
t.Fatal("dep")
}
if ErrorCode(ErrPlanActionForbidden) != "PLAN_ACTION_FORBIDDEN" {
t.Fatal("forbidden")
}
if ErrorCode(ErrPlanActionVersionConflict) != "PLAN_ACTION_VERSION_CONFLICT" {
t.Fatal("version")
}
}
func TestRESTPathsFrozen(t *testing.T) {
if PathActivePlan != "/api/v1/intent-plans/active" {
t.Fatal(PathActivePlan)
}
if PathPlanByID != "/api/v1/intent-plans/{plan_id}" {
t.Fatal(PathPlanByID)
}
if PathPlanDecisions != "/api/v1/intent-plans/{plan_id}/decisions" {
t.Fatal(PathPlanDecisions)
}
}
+137
View File
@@ -0,0 +1,137 @@
package intentflow
import (
"fmt"
"chat-agent-system/internal/intent"
)
const PolicyVersion = "batch-action-policy.v1"
// ActionBatchPolicy is the intentflow-owned batch row. It must not copy
// Risk, RequiredSlots, CapabilityKey, or AllowedStates from ActionRegistry.
type ActionBatchPolicy struct {
ActionKey string
RequiresBatchConfirm bool
OpenFromTier PlanTier
AllowedModes []ExecutionMode
RejectPlan bool
}
// BatchActionPolicy is the versioned fail-closed matrix keyed by ActionSpec.Key.
type BatchActionPolicy struct {
Version string
byKey map[string]ActionBatchPolicy
}
func DefaultBatchActionPolicy() *BatchActionPolicy {
p, err := NewBatchActionPolicy(defaultBatchRows(), intent.DefaultRegistry())
if err != nil {
panic(err)
}
return p
}
func NewBatchActionPolicy(rows []ActionBatchPolicy, registry *intent.ActionRegistry) (*BatchActionPolicy, error) {
if registry == nil {
return nil, fmt.Errorf("intentflow: action registry is required")
}
byKey := make(map[string]ActionBatchPolicy, len(rows))
for _, row := range rows {
if row.ActionKey == "" {
return nil, fmt.Errorf("intentflow: empty policy action key")
}
if _, exists := byKey[row.ActionKey]; exists {
return nil, fmt.Errorf("intentflow: duplicate policy key %q", row.ActionKey)
}
if _, ok := registry.Get(row.ActionKey); !ok {
return nil, fmt.Errorf("intentflow: unknown policy action key %q", row.ActionKey)
}
byKey[row.ActionKey] = row
}
for _, spec := range registry.All() {
if spec == nil {
continue
}
if _, ok := byKey[spec.Key]; !ok {
return nil, fmt.Errorf("intentflow: action %q missing batch policy", spec.Key)
}
}
return &BatchActionPolicy{Version: PolicyVersion, byKey: byKey}, nil
}
func (p *BatchActionPolicy) Lookup(key string) (ActionBatchPolicy, error) {
if p == nil {
return ActionBatchPolicy{}, fmt.Errorf("intentflow: policy is nil")
}
row, ok := p.byKey[key]
if !ok {
return ActionBatchPolicy{}, fmt.Errorf("intentflow: unknown action key %q", key)
}
return row, nil
}
func (p *BatchActionPolicy) RequiresBatchConfirm(key string) bool {
row, err := p.Lookup(key)
if err != nil {
return true
}
return row.RequiresBatchConfirm || row.RejectPlan
}
func defaultBatchRows() []ActionBatchPolicy {
autoRead := []string{
"regulation.query",
"case.query",
"permission.query",
"document.reject_reason.query",
"workflow.status.query",
"document.status.query",
"run.status.query",
"project.advisory",
"conversation.help",
}
confirmGenerate := []string{"task.run", "task.run.adhoc"}
rows := make([]ActionBatchPolicy, 0, len(intent.DefaultRegistry().All()))
seen := map[string]struct{}{}
add := func(row ActionBatchPolicy) {
rows = append(rows, row)
seen[row.ActionKey] = struct{}{}
}
for _, key := range autoRead {
add(ActionBatchPolicy{
ActionKey: key,
RequiresBatchConfirm: false,
OpenFromTier: TierAutoRead,
AllowedModes: []ExecutionMode{ModeLive, ModeValidatedOnly},
})
}
for _, key := range confirmGenerate {
add(ActionBatchPolicy{
ActionKey: key,
RequiresBatchConfirm: true,
OpenFromTier: TierConfirm,
AllowedModes: []ExecutionMode{ModeValidatedOnly, ModeLive},
})
}
add(ActionBatchPolicy{
ActionKey: "security.forbidden",
RejectPlan: true,
OpenFromTier: TierFull,
})
for _, spec := range intent.DefaultRegistry().All() {
if spec == nil {
continue
}
if _, ok := seen[spec.Key]; ok {
continue
}
add(ActionBatchPolicy{
ActionKey: spec.Key,
RequiresBatchConfirm: true,
OpenFromTier: TierFull,
AllowedModes: []ExecutionMode{ModeValidatedOnly},
})
}
return rows
}
+96
View File
@@ -0,0 +1,96 @@
package intentflow
import (
"reflect"
"strings"
"testing"
"chat-agent-system/internal/intent"
)
func TestDefaultPolicyCoversRegistryAndFailClosed(t *testing.T) {
p := DefaultBatchActionPolicy()
if p.Version != PolicyVersion {
t.Fatalf("version = %s", p.Version)
}
for _, spec := range intent.DefaultRegistry().All() {
if _, err := p.Lookup(spec.Key); err != nil {
t.Fatalf("registry key %s missing from policy: %v", spec.Key, err)
}
}
if _, err := p.Lookup("not.a.real.action"); err == nil {
t.Fatal("unknown key must fail closed")
}
}
func TestBatchConfirmMatrix(t *testing.T) {
p := DefaultBatchActionPolicy()
row, err := p.Lookup("task.run")
if err != nil {
t.Fatal(err)
}
if !row.RequiresBatchConfirm || row.OpenFromTier != TierConfirm {
t.Fatalf("task.run policy = %+v", row)
}
adhoc, err := p.Lookup("task.run.adhoc")
if err != nil {
t.Fatal(err)
}
if !adhoc.RequiresBatchConfirm {
t.Fatal("task.run.adhoc must require batch confirm")
}
query, err := p.Lookup("regulation.query")
if err != nil {
t.Fatal(err)
}
if query.RequiresBatchConfirm || query.OpenFromTier != TierAutoRead {
t.Fatalf("regulation.query policy = %+v", query)
}
forbidden, err := p.Lookup("security.forbidden")
if err != nil {
t.Fatal(err)
}
if !forbidden.RejectPlan {
t.Fatal("security.forbidden must reject the whole plan")
}
}
func TestUnknownPolicyKeyRejectedAtConstruction(t *testing.T) {
rows := append(defaultBatchRows(), ActionBatchPolicy{ActionKey: "invented.action"})
_, err := NewBatchActionPolicy(rows, intent.DefaultRegistry())
if err == nil {
t.Fatal("expected unknown policy key to fail")
}
}
func TestMissingRegistryKeyRejectedAtConstruction(t *testing.T) {
var trimmed []ActionBatchPolicy
for _, row := range defaultBatchRows() {
if row.ActionKey == "regulation.query" {
continue
}
trimmed = append(trimmed, row)
}
_, err := NewBatchActionPolicy(trimmed, intent.DefaultRegistry())
if err == nil {
t.Fatal("expected missing regulation.query policy to fail")
}
}
func TestActionSpecHasNoBatchOrTierFields(t *testing.T) {
rt := reflect.TypeOf(intent.ActionSpec{})
for i := 0; i < rt.NumField(); i++ {
name := strings.ToLower(rt.Field(i).Name)
for _, banned := range []string{"batch", "tier", "adapter", "hitl"} {
if strings.Contains(name, banned) {
t.Fatalf("ActionSpec.%s smuggles %s into L1", rt.Field(i).Name, banned)
}
}
}
}
func TestValidatedOnlyIsNotSuccess(t *testing.T) {
if ModeValidatedOnly == ExecutionMode(PlanCompleted) || ModeValidatedOnly == ExecutionMode(StepSucceeded) {
t.Fatal("validated_only must stay distinct from succeeded/completed")
}
}
+14
View File
@@ -0,0 +1,14 @@
package intentflow
import "context"
// Service is the only surface chat/server may call. Implementations live in
// later units; U1 freezes the signature so HTTP and chat cannot grow a second
// plan owner.
type Service interface {
HandleTurn(ctx context.Context, in TurnInput) (*TurnResult, error)
DecidePlan(ctx context.Context, in DecisionCommand) (*PlanView, error)
GetPlan(ctx context.Context, principal Principal, planID string) (*PlanView, error)
// GetActivePlan returns (nil, nil) when the principal has no visible active plan.
GetActivePlan(ctx context.Context, principal Principal) (*PlanView, error)
}
+121
View File
@@ -0,0 +1,121 @@
package intentflow
import "time"
// PlanStatus is the authoritative plan state machine (design §5.1).
type PlanStatus string
const (
PlanProposed PlanStatus = "proposed"
PlanAwaitingClarification PlanStatus = "awaiting_clarification"
PlanAwaitingConfirmation PlanStatus = "awaiting_confirmation"
PlanExecuting PlanStatus = "executing"
PlanPartiallyCompleted PlanStatus = "partially_completed"
PlanCompleted PlanStatus = "completed"
PlanFailed PlanStatus = "failed"
PlanCancelled PlanStatus = "cancelled"
PlanExpired PlanStatus = "expired"
PlanSuperseded PlanStatus = "superseded"
)
// StepStatus is the authoritative step state machine (design §5.2).
type StepStatus string
const (
StepPending StepStatus = "pending"
StepEligible StepStatus = "eligible"
StepRunning StepStatus = "running"
StepSucceeded StepStatus = "succeeded"
StepFailed StepStatus = "failed"
StepDenied StepStatus = "denied"
StepConflicted StepStatus = "conflicted"
StepBlockedDependency StepStatus = "blocked_dependency"
StepSkipped StepStatus = "skipped"
StepCancelled StepStatus = "cancelled"
)
// ExecutionMode distinguishes dry-run precheck from a real action.
// validated_only must never be written as succeeded/completed.
type ExecutionMode string
const (
ModeValidatedOnly ExecutionMode = "validated_only"
ModeLive ExecutionMode = "live"
)
// DecisionKind is the unified server command for buttons and natural language.
type DecisionKind string
const (
DecisionConfirm DecisionKind = "confirm"
DecisionCancel DecisionKind = "cancel"
DecisionExclude DecisionKind = "exclude"
DecisionClarify DecisionKind = "clarify"
DecisionEdit DecisionKind = "edit"
DecisionConfirmRetry DecisionKind = "confirm_retry"
DecisionContinue DecisionKind = "continue"
DecisionReplace DecisionKind = "replace"
)
// Principal is taken only from the authenticated session. Callers must not
// copy user/project/session identifiers from the request body into these fields.
type Principal struct {
UserID int64
Environment string
ProjectID int64
SessionID string
}
// TurnInput is one user utterance plus client idempotency.
type TurnInput struct {
Principal Principal
Text string
ClientTurnID string
RequestDigest string
}
// TurnResult is the HandleTurn projection. No plan is a documented empty state.
type TurnResult struct {
Plan *PlanView
}
// DecisionCommand is the single write path for confirm/cancel/edit/exclude.
type DecisionCommand struct {
Principal Principal
PlanID string
ExpectedRevision int64
DecisionNonce string
DecisionDigest string
Kind DecisionKind
StepIDs []string
ExcludeUnselected bool
}
// PlanView is the desensitized UI/API projection. It must not return internal
// payloads, permission snapshots, or full intermediate results.
type PlanView struct {
PlanID string `json:"plan_id"`
Revision int64 `json:"revision"`
Status PlanStatus `json:"status"`
ExecutionMode ExecutionMode `json:"execution_mode"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
Steps []StepView `json:"steps,omitempty"`
SafeSummary string `json:"safe_summary,omitempty"`
}
// StepView is the desensitized step projection.
type StepView struct {
StepID string `json:"step_id"`
ActionKey string `json:"action_key"`
Status StepStatus `json:"status"`
ExecutionMode ExecutionMode `json:"execution_mode"`
DependsOn []string `json:"depends_on,omitempty"`
SafeSummary string `json:"safe_summary,omitempty"`
}
// REST paths are frozen here; U5 mounts them on the authenticated server.
const (
PathActivePlan = "/api/v1/intent-plans/active"
PathPlanByID = "/api/v1/intent-plans/{plan_id}"
PathPlanDecisions = "/api/v1/intent-plans/{plan_id}/decisions"
)
+2 -2
View File
@@ -14,8 +14,8 @@ import (
const internalPrefix = "chat-agent-system/internal/"
var (
sqlWriteRe = regexp.MustCompile(`(?is)\b(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)`)
eventNameRe = regexp.MustCompile(`document\.[a-z0-9_.]+`)
sqlWriteRe = regexp.MustCompile(`(?is)\b(INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)`)
eventNameRe = regexp.MustCompile(`(?:document|intent\.plan)\.[a-z0-9_.]+`)
)
type fileClass int
+14 -8
View File
@@ -12,14 +12,17 @@ const (
ConsumerWorkflowAdvance = "workflow_advance"
ConsumerFAV = "fav"
EventCandidateReady = "document.agent_candidate_ready"
EventVersionCommitted = "document.agent_version_committed"
EventDownstreamImpact = "document.downstream_impact_detected"
EventChangeReportDegraded = "document.change_report_degraded"
EventApproved = "document.approved"
EventTaskRunCompleted = "taskrun.completed"
EventTaskRunCancelled = "taskrun.cancelled"
EventFAVArtifactStaged = "fav.artifact_staged"
EventCandidateReady = "document.agent_candidate_ready"
EventVersionCommitted = "document.agent_version_committed"
EventDownstreamImpact = "document.downstream_impact_detected"
EventChangeReportDegraded = "document.change_report_degraded"
EventApproved = "document.approved"
EventTaskRunCompleted = "taskrun.completed"
EventTaskRunCancelled = "taskrun.cancelled"
EventFAVArtifactStaged = "fav.artifact_staged"
EventIntentPlanAwaiting = "intent.plan.awaiting_input"
EventIntentPlanStepChanged = "intent.plan.step_changed"
EventIntentPlanCompleted = "intent.plan.completed"
)
// Contract is the static allowlist row for one event name.
@@ -44,6 +47,9 @@ func StaticContracts() []Contract {
{EventName: EventTaskRunCompleted, SchemaVersion: 1, Enabled: true, Producers: []string{"taskrun"}, Consumers: []string{ConsumerNotify}, PayloadFields: []string{"run_id"}},
{EventName: EventTaskRunCancelled, SchemaVersion: 1, Enabled: true, Producers: []string{"taskrun"}, Consumers: []string{ConsumerNotify}, PayloadFields: []string{"run_id"}},
{EventName: EventFAVArtifactStaged, SchemaVersion: 1, Enabled: false, Producers: []string{"app/docflow"}, Consumers: []string{ConsumerFAV}, PayloadFields: []string{"doc_id", "snapshot_id"}},
{EventName: EventIntentPlanAwaiting, SchemaVersion: 1, Enabled: true, Producers: []string{"app/intentflow", "events"}, Consumers: []string{ConsumerProjection, ConsumerNotify}, PayloadFields: []string{"plan_id", "revision", "status", "environment", "project_id", "user_id", "session_id", "summary_ref"}},
{EventName: EventIntentPlanStepChanged, SchemaVersion: 1, Enabled: true, Producers: []string{"app/intentflow", "events"}, Consumers: []string{ConsumerProjection, ConsumerNotify}, PayloadFields: []string{"plan_id", "revision", "step_id", "status", "environment", "project_id", "user_id", "session_id", "summary_ref"}},
{EventName: EventIntentPlanCompleted, SchemaVersion: 1, Enabled: true, Producers: []string{"app/intentflow", "events"}, Consumers: []string{ConsumerProjection, ConsumerNotify}, PayloadFields: []string{"plan_id", "revision", "status", "environment", "project_id", "user_id", "session_id", "summary_ref"}},
}
}
+25
View File
@@ -49,6 +49,31 @@ func TestManifestJSONIncludesDisabledFAV(t *testing.T) {
}
}
func TestIntentPlanContractsOmitSensitivePayload(t *testing.T) {
forbidden := []string{"prompt", "slots", "permissions", "result", "input"}
for _, c := range StaticContracts() {
if !strings.HasPrefix(c.EventName, "intent.plan.") {
continue
}
if !c.Enabled {
t.Fatalf("%s must be enabled for U4 append", c.EventName)
}
if !containsName(c.Producers, "app/intentflow") || !containsName(c.Producers, "events") {
t.Fatalf("%s producers = %v", c.EventName, c.Producers)
}
if !containsName(c.Consumers, ConsumerProjection) || !containsName(c.Consumers, ConsumerNotify) {
t.Fatalf("%s consumers = %v", c.EventName, c.Consumers)
}
for _, field := range c.PayloadFields {
for _, bad := range forbidden {
if field == bad {
t.Fatalf("%s payload contains %s", c.EventName, field)
}
}
}
}
}
func TestRegistryParityWithYAML(t *testing.T) {
root := moduleRoot(t)
b, err := archtest.LoadBoundaries(archtest.DefaultBoundariesPath(root))