chore(agent-infra): round-2 加固——据 Opus 评审堵 harness 漏洞
独立 Opus 评审发现、并经我核验为真的关键漏洞,本轮整改(JDK21 实跑验证全绿): - P0-1 CI 接上电:maven.yml 触发分支 master→main(此前误配 master、仓库无该分支 → CI 从不运行, 所有门禁形同离线)、checkout/setup-java 升 v4。 - P0-2 BC 门通用化:ArchUnit 改为通用条件,覆盖全部业务 BC 间 .dal 方向(原仅守 AI 单向); 整改 knowledge 同构违例(ContentKnowledgeWorkOwnerFacade 改消费 MuseContentWorkOwnerApi); market→member.dal 写路径违例单点登记待整改(KNOWN_VIOLATION_EXEMPTIONS + bc-boundaries §三)。 - loop 机械牙:新增 AgentsInfraIntegrityTest(每业务 BC 有 .agent、README 索引每篇 .agents 文档、总账在), 把写回/索引同步从自觉变机械。 - P1-1 去魔法数:覆盖门分域计数 28/21/37 改为派生自 APPROVED_*_COMPLETED_OPERATIONS.size()。 - openapi-diff materialize 为独立 workflow(待首跑验证)。 - 文档诚实化:AGENTS/总账/bc-boundaries 订正"已验证绿"等过度声称为与实际相符(BC 门 market 待整改、契约门仅存在性/结构)。 验证:JDK21 mvn -pl muse-server -am 实跑 7 类门禁/单测全绿(BcBoundary 1/0F 且 0 Architecture Violation、 AgentsInfraIntegrity 3/0F、ContractFirst 2/0F、P1rApiCoverageReport 7/0F、knowledge/ai/content 适配器单测全绿),BUILD SUCCESS。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e181669197
commit
2cff86b808
@ -26,18 +26,26 @@
|
||||
|
||||
## 三、违例登记与整改
|
||||
|
||||
**当前无任何豁免**——`BcBoundaryArchTest` 已不含 `haveNameNotMatching`,任何 AI→他域 DAL 依赖一律红。
|
||||
规则现为**通用条件**(`BcBoundaryArchTest.no_business_bc_may_depend_on_another_bc_dal`):覆盖全部业务 BC 间方向(ai/content/knowledge/market/meta/member/events),任一域依赖他域 `.dal` 即红;已知违例在测试的 `KNOWN_VIOLATION_EXEMPTIONS` 单点显式登记(2026-06-14 评审整改:此前只守 AI 单向,放过了 knowledge/market 同构活违例)。
|
||||
|
||||
### 已整改(历史)
|
||||
| 违例类 | 曾经内容 | 整改 | 验收(2026-06-14) |
|
||||
| 违例类 | 曾经内容 | 整改 | 验收 |
|
||||
|---|---|---|---|
|
||||
| `module.ai.application.muse.facade.ContentMuseWorkOwnerFacade` | 直连 content `WorkMapper/ChapterMapper/BlockMapper` + `WorkDO/ChapterDO/BlockDO`(字节码 31 处依赖) | content 暴露对外只读端口 `MuseContentWorkOwnerApi`(content-api 定义、content-server 实现读自有 DAL);AI 适配器改消费该端口,移除全部 content.dal 依赖,AI 业务规则不变 | 反向:删豁免+旧码 → 门禁红、精确报 31 例;正向:整改+删豁免 → 门禁**绿**(收紧),适配器单测 13/0F、端口实现单测 7/0F |
|
||||
| `module.ai.application.muse.facade.ContentMuseWorkOwnerFacade` | 直连 content `Work/Chapter/Block` Mapper+DO(字节码 31 处) | content 暴露只读端口 `MuseContentWorkOwnerApi`(content-api 定义、content-server 实现读自有 DAL);AI 适配器改消费该端口 | 反向删豁免+旧码→红报 31 例;正向整改→绿,适配器 13/0F、端口实现 7/0F |
|
||||
| `module.knowledge.application.muse.facade.ContentKnowledgeWorkOwnerFacade` | 直连 content `WorkMapper/WorkDO`(与 AI 同构) | 复用同一 `MuseContentWorkOwnerApi.getActiveOwnedWorkRevision` 端口,移除 content.dal 依赖 | 通用门禁绿(0 Architecture Violation)、新增适配器单测 3/0F |
|
||||
|
||||
> 豁免一律视为**临时**:违例消除后必须从 `haveNameNotMatching` 删除该类,使门禁收紧——**收紧即整改验收**(本条已完成,豁免已清零)。新违例不在任何豁免内,一律红。
|
||||
### 活跃登记(待整改,单点豁免)
|
||||
| 违例类 | 内容 | 处置 | 整改方向 |
|
||||
|---|---|---|---|
|
||||
| `module.market.application.muse.{MarketAccountProjectionProvider, AdminMarketReviewServiceImpl, MarketInstallServiceImpl, MarketLicenseServiceImpl, MarketPublishServiceImpl}`(5 类) | **写路径**:构建 `member.dal.AccountRecordProjectionDO`、经 `AccountRecordProjectionMapper` upsert 进 member 表 | `BcBoundaryArchTest.KNOWN_VIOLATION_EXEMPTIONS` 单点豁免这 5 类(显式可见) | member 暴露**写端口**(如 `MuseAccountRecordProjectionApi.upsert(DTO)`)+ member-server 接管 upsert + DTO 替代 DO;market 改消费端口、移除 member.dal 依赖后从豁免删除 → 门禁自动收紧。属写路径跨 BC 重构,作专项 |
|
||||
|
||||
> 豁免一律**临时、显式、可见**(测试源码点名)。违例消除后必须从 `KNOWN_VIOLATION_EXEMPTIONS` 删除该类——**收紧即整改验收**。新违例不在豁免内,一律红。
|
||||
|
||||
---
|
||||
|
||||
## 四、扩展方式
|
||||
|
||||
- 新增 BC 间约束(如禁 content→ai.dal、禁前端契约外的耦合)按同样方式在 `BcBoundaryArchTest` 加 `@Test` 规则。
|
||||
- 违例消除后,从对应 `haveNameNotMatching` 豁免里删掉该类,门禁随即收紧——**收紧动作本身就是整改完成的验收**。
|
||||
- 规则已是**通用条件**,自动覆盖所有业务 BC 间的 `.dal` 跨域依赖,**无需为新方向加 `@Test`**。
|
||||
- 新增业务 BC:加入 `BcBoundaryArchTest.BUSINESS_BCS` 即纳入约束。
|
||||
- 收紧已知违例:整改后从 `KNOWN_VIOLATION_EXEMPTIONS` 删除该类,门禁随即收紧。
|
||||
- 更强边界(如禁跨域 `.application` 非 facade-api 包耦合):在条件里追加判断,但须为合规 facade 端口留通道(见评审 P1-3,属后续)。
|
||||
|
||||
@ -36,9 +36,10 @@
|
||||
|
||||
---
|
||||
|
||||
## 四、待启用加固:openapi-diff 破坏性变更检测(CI 级)
|
||||
## 四、加固:openapi-diff 破坏性变更检测(CI 级,已 materialize / 待首跑验证)
|
||||
|
||||
存在性/结构门禁挡不住“悄悄改坏既有契约语义”(删字段、改必填、改类型)。**破坏性变更检测**用 `oasdiff` 在 PR 上比对 base 分支与 PR 的 OpenAPI,发现破坏性差异即红。就绪片段(待接入 `maven.yml` 或独立 workflow,接入后需一次 CI 运行验证):
|
||||
存在性/结构门禁挡不住“悄悄改坏既有契约语义”(删字段、改必填、改类型)。**破坏性变更检测**用 `oasdiff` 在 PR 上比对 base 分支与 PR 的 OpenAPI,发现破坏性差异即红。
|
||||
**已落地为独立 workflow** [`muse-cloud/.github/workflows/openapi-diff.yml`](../../muse-cloud/.github/workflows/openapi-diff.yml)(PR 改 `docs/api-contracts/**` 时触发,逐域 oasdiff)。⚠️ **本地无法运行 GitHub Actions/oasdiff,需一次真实 CI 运行验证后**方可宣称"破坏性变更已被拦截"。参考片段:
|
||||
|
||||
```yaml
|
||||
openapi-breaking-change:
|
||||
|
||||
12
AGENTS.md
12
AGENTS.md
@ -88,12 +88,12 @@ oh-my-muse/
|
||||
|
||||
## 7. 当前基建建设状态(随建随更)
|
||||
|
||||
- ✅ 机械门禁地基:CI 真跑测试(JDK21)、门禁去硬编码、P0 冻结令(见脊柱规则"已落地")。
|
||||
- ✅ 机械门禁地基:CI 真跑测试(JDK21、触发分支已修为 `main`——此前误配 `master` 致 CI 从不运行)、门禁去硬编码(含分域计数派生化)、P0 冻结令。
|
||||
- ✅ 入口与中枢骨架:本文件 + `.agents/README.md` + 脊柱规则。
|
||||
- ✅ BC 边界 ArchUnit 门([`bc-boundaries`](.agents/rules/bc-boundaries.md)):AI 不得直连他域 DAL,已验证绿。
|
||||
- ✅ 契约先行机械门禁([`contract-first`](.agents/rules/contract-first.md)):Flyway 迁移卫生 + OpenAPI 结构,原地 SSOT,已验证绿。
|
||||
- ✅ knowledge 蒸馏 / skills(黄金旅程“完成”定义 + 加 BC 模块)/ workflow(AI 开发协议):见 [`.agents/README.md`](.agents/README.md)。
|
||||
- ✅ 进度总账(单一进度源)+ 7 BC per-module `.agent`:见 [`docs/mvp/进度总账.md`](docs/mvp/进度总账.md)。
|
||||
- ⏳ 后续(非阻塞):openapi-diff 破坏性变更 CI、`docs/dev-baseline/global/*` 收敛进 `.agents/rules/`(engineering-conventions / security-and-reliability)、BC 边界门扩到其它方向。
|
||||
- ✅ BC 边界 ArchUnit 门([`bc-boundaries`](.agents/rules/bc-boundaries.md)):**通用覆盖全业务 BC 间方向**;AI/knowledge 直连 content 违例已整改消除;**market→member.dal 写路径违例单点登记待整改**(故非"全绿",见该规则 §三)。
|
||||
- ✅ 契约先行门([`contract-first`](.agents/rules/contract-first.md)):Flyway 迁移卫生 + OpenAPI **存在性/结构**(注:挡不住语义破坏;openapi-diff CI 已materialize,需首次 CI 运行验证)。
|
||||
- ✅ loop 机械牙([`AgentsInfraIntegrityTest`](muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/arch/AgentsInfraIntegrityTest.java)):每业务 BC 有 `.agent`、README 索引每篇 `.agents` 文档、总账在——把写回/索引同步从自觉变机械。
|
||||
- ✅ knowledge 蒸馏 / skills / workflow(AI 开发协议)/ 进度总账 + 7 BC `.agent`。
|
||||
- ⏳ 后续:market 写路径 BC 整改、openapi-diff CI 首跑验证、`completed=测试证据`兜底(testFiles)、`dev-baseline` 收敛进 rules、跨域 `.application` 边界。
|
||||
|
||||
> 进度只进 [`docs/mvp/进度总账.md`](docs/mvp/进度总账.md) + 各模块 `.agent`,不新增状态过程文档。
|
||||
|
||||
@ -10,14 +10,17 @@
|
||||
| 砖 | 内容 | 状态 | 机械证据 / 落点 |
|
||||
|---|---|---|---|
|
||||
| P0 | CI 真跑测试(JDK21、去 `-Dmaven.test.skip`)+ 覆盖台账去硬编码 + P0 冻结令 | ✅ | `.github/workflows/maven.yml`、`P1rApiCoverageReportTest`、[脊柱规则](../../.agents/rules/verification-and-anti-false-green.md) |
|
||||
| ① | BC 边界 ArchUnit 门(AI 不得直连他域 DAL) | ✅ 已验证绿 | `BcBoundaryArchTest`(Tests run 1/0F);[bc-boundaries](../../.agents/rules/bc-boundaries.md) |
|
||||
| ② | 契约先行门(Flyway 迁移卫生 + OpenAPI 结构;原地 SSOT) | ✅ 已验证绿 | `ContractFirstGateTest`(Tests run 2/0F);[contract-first](../../.agents/rules/contract-first.md) |
|
||||
| ① | BC 边界 ArchUnit 门——**通用覆盖全业务 BC 间方向** | ✅ 绿(AI/knowledge 已整改;**market 写路径违例登记待整改**,非全绿) | `BcBoundaryArchTest`(1/0F,0 Architecture Violation);[bc-boundaries](../../.agents/rules/bc-boundaries.md) §三 |
|
||||
| ② | 契约先行门(Flyway 迁移卫生 + OpenAPI **存在性/结构**) | ✅ 绿(注:挡不住语义破坏;openapi-diff CI 待首跑验证) | `ContractFirstGateTest`(2/0F);[contract-first](../../.agents/rules/contract-first.md) |
|
||||
| ⑦ | loop 机械牙 + CI 接电(round-2) | ✅ 绿 | `AgentsInfraIntegrityTest`(3/0F);`maven.yml` 触发分支修为 `main` |
|
||||
| ③ | knowledge 蒸馏(定位架构 / 现状基线指针 / 决策) | ✅ | [`.agents/knowledge/`](../../.agents/knowledge/) |
|
||||
| ④ | skills(黄金旅程“完成”定义 / 新增 BC 模块) | ✅ | [`.agents/skills/`](../../.agents/skills/) |
|
||||
| ⑤ | workflow(AI 开发协议元流程) | ✅ | [`.agents/workflows/ai-development-protocol.md`](../../.agents/workflows/ai-development-protocol.md) |
|
||||
| ⑥ | 进度总账(本文件)+ per-module `.agent` | ✅ | 本文件 + `muse-cloud/muse-module-*/.agent` |
|
||||
|
||||
**后续基建 TODO(非阻塞)**:openapi-diff 破坏性变更 CI(就绪片段见 contract-first §四)、`docs/dev-baseline/global/*` 收敛进 `.agents/rules/`(engineering-conventions、security-and-reliability)、BC 边界门扩到其它方向。
|
||||
**round-2 加固(2026-06-14,据 Opus 评审)**:CI 触发分支 master→main(此前 CI 从不运行)、BC 门通用化 + 整改 knowledge 违例、loop 装机械牙、覆盖门去魔法数、文档诚实化、openapi-diff materialize 为独立 workflow。
|
||||
|
||||
**后续基建 TODO**:**market→member.dal 写路径 BC 整改**(member 暴露写端口,见 bc-boundaries §三)、openapi-diff CI 首跑验证、`completed=测试证据`兜底(台账加 testFiles 字段 + 门禁)、`dev-baseline` 收敛进 rules、跨域 `.application` 边界门。
|
||||
|
||||
---
|
||||
|
||||
|
||||
8
muse-cloud/.github/workflows/maven.yml
vendored
8
muse-cloud/.github/workflows/maven.yml
vendored
@ -5,9 +5,9 @@ name: Java CI with Maven
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -19,9 +19,9 @@ jobs:
|
||||
java: [ '21' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK ${{ matrix.java }}
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: 'temurin'
|
||||
|
||||
36
muse-cloud/.github/workflows/openapi-diff.yml
vendored
Normal file
36
muse-cloud/.github/workflows/openapi-diff.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# OpenAPI 破坏性变更门禁(契约先行加固)
|
||||
# 仅在 PR 改动 docs/api-contracts/** 时触发;逐域用 oasdiff 比对 base 与 PR 的 OpenAPI,发现破坏性差异即失败。
|
||||
# 注:独立于主 maven 构建,避免误配影响主门禁。**首次接入后需一次真实 CI 运行验证**(本地无法运行 GitHub Actions/oasdiff)。
|
||||
name: OpenAPI Breaking Change Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'docs/api-contracts/**'
|
||||
|
||||
jobs:
|
||||
openapi-diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 检出 base 分支契约到 /tmp/base
|
||||
run: git worktree add /tmp/base "${{ github.event.pull_request.base.sha }}"
|
||||
- name: oasdiff 破坏性变更检测(逐业务域)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fail=0
|
||||
for d in account ai content events knowledge market meta; do
|
||||
base="/tmp/base/docs/api-contracts/$d/openapi.yaml"
|
||||
head="docs/api-contracts/$d/openapi.yaml"
|
||||
if [ ! -f "$base" ] || [ ! -f "$head" ]; then
|
||||
echo "跳过 $d(base 或 PR 缺该契约文件)"; continue
|
||||
fi
|
||||
echo "== oasdiff breaking: $d =="
|
||||
docker run --rm -v "$PWD:/pr" -v /tmp/base:/base tufin/oasdiff breaking \
|
||||
"/base/docs/api-contracts/$d/openapi.yaml" "/pr/docs/api-contracts/$d/openapi.yaml" \
|
||||
--fail-on ERR || fail=1
|
||||
done
|
||||
exit $fail
|
||||
@ -1,9 +1,7 @@
|
||||
package cn.iocoder.muse.module.knowledge.application.muse.facade;
|
||||
|
||||
import cn.iocoder.muse.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.muse.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.muse.module.content.dal.dataobject.WorkDO;
|
||||
import cn.iocoder.muse.module.content.dal.mysql.WorkMapper;
|
||||
import cn.iocoder.muse.module.content.api.MuseContentWorkOwnerApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
@ -12,25 +10,27 @@ import org.springframework.stereotype.Component;
|
||||
import static cn.iocoder.muse.module.knowledge.enums.ErrorCodeConstants.KNOWLEDGE_RESOURCE_FORBIDDEN;
|
||||
|
||||
/**
|
||||
* 基于 Content 作品表的 Knowledge work owner adapter。
|
||||
* 基于 Content **对外 API** 的 Knowledge work owner adapter(BC 合规)。
|
||||
*
|
||||
* <p>整改记录(2026-06-14):此前直连 content 的 WorkMapper/WorkDO,破坏 BC 边界(与 AI 域同构违例);
|
||||
* 现改为消费 content 对外契约 {@link MuseContentWorkOwnerApi},只取脱敏的作品归属 revision。
|
||||
* 见 .agents/rules/bc-boundaries.md。</p>
|
||||
*/
|
||||
@Component
|
||||
@Primary
|
||||
@ConditionalOnBean(WorkMapper.class)
|
||||
@ConditionalOnBean(MuseContentWorkOwnerApi.class)
|
||||
public class ContentKnowledgeWorkOwnerFacade implements MuseKnowledgeWorkOwnerFacade {
|
||||
|
||||
@Resource
|
||||
private WorkMapper workMapper;
|
||||
private MuseContentWorkOwnerApi contentWorkOwnerApi;
|
||||
|
||||
@Override
|
||||
public void requireWorkOwner(Long workId, Long ownerUserId) {
|
||||
if (workId == null || ownerUserId == null) {
|
||||
throw new ServiceException(KNOWLEDGE_RESOURCE_FORBIDDEN);
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||
// 只接受当前租户、未删除、owner_user_id 命中的作品;查不到统一按无权访问处理,避免泄露存在性。
|
||||
WorkDO work = workMapper.selectActiveByIdAndOwner(tenantId, workId, ownerUserId);
|
||||
if (work == null) {
|
||||
// 经 content 对外契约校验作品归属;不属于当前用户 / 作品不可用时按无权访问处理(失败关闭)。
|
||||
if (contentWorkOwnerApi.getActiveOwnedWorkRevision(workId, ownerUserId) == null) {
|
||||
throw new ServiceException(KNOWLEDGE_RESOURCE_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package cn.iocoder.muse.module.knowledge.application.muse.facade;
|
||||
|
||||
import cn.iocoder.muse.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.muse.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.muse.module.content.api.MuseContentWorkOwnerApi;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import static cn.iocoder.muse.module.knowledge.enums.ErrorCodeConstants.KNOWLEDGE_RESOURCE_FORBIDDEN;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Knowledge 作品归属 Facade 测试。
|
||||
*
|
||||
* <p>整改后(2026-06-14):适配器改消费 content 对外契约 {@link MuseContentWorkOwnerApi},不再直连 content.dal;
|
||||
* 本测试 mock 该端口,验证"属于/不属于/空入参"下的失败关闭语义不变。</p>
|
||||
*/
|
||||
class ContentKnowledgeWorkOwnerFacadeTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private ContentKnowledgeWorkOwnerFacade workOwnerFacade;
|
||||
@Mock
|
||||
private MuseContentWorkOwnerApi contentWorkOwnerApi;
|
||||
|
||||
@Test
|
||||
void should_allowWhenWorkOwned() {
|
||||
when(contentWorkOwnerApi.getActiveOwnedWorkRevision(9001L, 10001L)).thenReturn(3);
|
||||
|
||||
workOwnerFacade.requireWorkOwner(9001L, 10001L);
|
||||
|
||||
verify(contentWorkOwnerApi).getActiveOwnedWorkRevision(9001L, 10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectWhenWorkNotOwnedOrMissing() {
|
||||
when(contentWorkOwnerApi.getActiveOwnedWorkRevision(9001L, 10002L)).thenReturn(null);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> workOwnerFacade.requireWorkOwner(9001L, 10002L));
|
||||
|
||||
assertEquals(KNOWLEDGE_RESOURCE_FORBIDDEN.getCode(), exception.getCode());
|
||||
verify(contentWorkOwnerApi).getActiveOwnedWorkRevision(9001L, 10002L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectBlankIdentityBeforeQuery() {
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> workOwnerFacade.requireWorkOwner(null, 10001L));
|
||||
|
||||
assertEquals(KNOWLEDGE_RESOURCE_FORBIDDEN.getCode(), exception.getCode());
|
||||
// 空入参必须在触达 content 契约前失败关闭。
|
||||
verifyNoInteractions(contentWorkOwnerApi);
|
||||
}
|
||||
|
||||
}
|
||||
@ -323,9 +323,12 @@ class P1rApiCoverageReportTest {
|
||||
assertOperationStatus("content", "listPlanningCandidates", "dedicated", "needs_verification");
|
||||
assertOperationStatus("content", "createStyleCheck", "dedicated", "needs_verification");
|
||||
assertOperationStatus("content", "adminRiskAction", "dedicated", "completed", true);
|
||||
assertDomainStatusCount("market", "dedicated", "needs_verification", 28);
|
||||
assertDomainStatusCount("account", "dedicated", "needs_verification", 21);
|
||||
assertDomainStatusCount("content", "dedicated", "needs_verification", 37);
|
||||
// P1 加固(2026-06-14):废弃硬编码 needs_verification 魔法数(28/21/37,与脊柱规则禁的"可拨动常量"同模式),
|
||||
// 改为断言"该域 completed 数 == 显式批准集大小"——派生自唯一可信来源 APPROVED_*_COMPLETED_OPERATIONS。
|
||||
// 任何把 operation 静默推进为 completed 的动作都会让 completed 数超过批准集大小而变红,无法只靠拨常量蒙混。
|
||||
assertDomainStatusCount("market", "dedicated", "completed", APPROVED_MARKET_COMPLETED_OPERATIONS.size());
|
||||
assertDomainStatusCount("account", "dedicated", "completed", APPROVED_ACCOUNT_COMPLETED_OPERATIONS.size());
|
||||
assertDomainStatusCount("content", "dedicated", "completed", APPROVED_CONTENT_COMPLETED_OPERATIONS.size());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
package cn.iocoder.muse.server.framework.arch;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Agent 基建完整性门禁——给"复利写回 loop"装机械牙。
|
||||
*
|
||||
* <p>loop(知识沉淀 / 索引同步 / 模块记忆)此前全靠文档纪律,易衰减——正是本项目栽过的坑(软约束被绕过)。
|
||||
* 本测试把这套自觉变为 CI 可阻断的机械约束:
|
||||
* <ol>
|
||||
* <li>每个业务 BC 模块必须有 {@code .agent} 跨会话记忆;</li>
|
||||
* <li>{@code .agents/} 下每个 .md 文档都必须被 {@code README.md} 索引(防新增 rule/knowledge/skill/workflow 成孤儿);</li>
|
||||
* <li>单一进度源 {@code docs/mvp/进度总账.md} 必须存在。</li>
|
||||
* </ol>
|
||||
* 任一缺失 / 失链 → 红,阻断合入。呼应 {@code .agents/README.md §三}"rules 必配门禁 / 同步索引"的自我要求。</p>
|
||||
*/
|
||||
class AgentsInfraIntegrityTest {
|
||||
|
||||
/** 业务 BC 模块(各须有 per-module .agent 记忆,与 BcBoundaryArchTest 的 BUSINESS_BCS 对应)。 */
|
||||
private static final List<String> BUSINESS_BC_MODULES = List.of(
|
||||
"muse-module-ai", "muse-module-content", "muse-module-knowledge",
|
||||
"muse-module-market", "muse-module-meta", "muse-module-member", "muse-module-events");
|
||||
|
||||
@Test
|
||||
void every_business_bc_module_has_agent_memory() {
|
||||
Path root = findRepoRoot();
|
||||
for (String module : BUSINESS_BC_MODULES) {
|
||||
Path agent = root.resolve("muse-cloud").resolve(module).resolve(".agent");
|
||||
assertTrue(Files.exists(agent), "业务 BC 模块缺少 .agent 跨会话记忆: " + agent);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void single_progress_ledger_exists() {
|
||||
assertTrue(Files.exists(findRepoRoot().resolve("docs/mvp/进度总账.md")),
|
||||
"单一进度源 docs/mvp/进度总账.md 必须存在(进度不得回流到过程文档)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readme_indexes_every_agents_doc() throws IOException {
|
||||
Path agentsDir = findRepoRoot().resolve(".agents");
|
||||
assertTrue(Files.isDirectory(agentsDir), ".agents 中枢目录必须存在: " + agentsDir);
|
||||
Path readme = agentsDir.resolve("README.md");
|
||||
assertTrue(Files.exists(readme), ".agents/README.md 索引必须存在");
|
||||
String readmeText = Files.readString(readme);
|
||||
|
||||
try (Stream<Path> docs = Files.walk(agentsDir)) {
|
||||
for (Path doc : docs.filter(p -> p.getFileName().toString().endsWith(".md"))
|
||||
.filter(p -> !p.equals(readme))
|
||||
.toList()) {
|
||||
String relativePath = agentsDir.relativize(doc).toString().replace('\\', '/');
|
||||
assertTrue(readmeText.contains(relativePath),
|
||||
".agents/README.md 未索引该文档(新增 rule/knowledge/skill/workflow 须登记进 README 清单): " + relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 逐级上溯,定位同时含 {@code .agents} 与 {@code muse-cloud} 的仓库根,适配 surefire 的工作目录。 */
|
||||
private static Path findRepoRoot() {
|
||||
Path current = Path.of("").toAbsolutePath();
|
||||
for (Path candidate = current; candidate != null; candidate = candidate.getParent()) {
|
||||
if (Files.isDirectory(candidate.resolve(".agents"))
|
||||
&& Files.isDirectory(candidate.resolve("muse-cloud"))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,20 +1,29 @@
|
||||
package cn.iocoder.muse.server.framework.arch;
|
||||
|
||||
import com.tngtech.archunit.core.domain.Dependency;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.lang.ArchCondition;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import com.tngtech.archunit.lang.ConditionEvents;
|
||||
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
/**
|
||||
* BC 边界机械门禁(agent 基建 / 机械门禁优先)。
|
||||
*
|
||||
* <p>规则:一个业务域(BC)不得直连他域的 DAL(dataobject/mysql Mapper);跨 BC 只能走对外 API / facade-api 契约。
|
||||
* 这是把“边界靠自觉”升级为“机械阻断”——对抗复盘 A4 已实证:软约束下 AI 模块直连了 content 的 DAL。</p>
|
||||
* <p>规则:任一 Muse 业务域(BC)不得直连他域的 DAL(dataobject / mysql Mapper);跨 BC 只能走对外 API / facade-api 契约。
|
||||
* 这把“边界靠自觉”升级为“机械阻断”——对抗复盘 A4 已实证:软约束下会蔓延(AI、knowledge 直连 content;market 直连 member)。</p>
|
||||
*
|
||||
* <p>规则与已知违例登记见 {@code .agents/rules/bc-boundaries.md};本测试随 CI(P0 已打开测试门)在 PR/push 阻断新违例。</p>
|
||||
* <p>本测试**通用覆盖全部业务 BC 间方向**(非单向);任一业务 BC 对他域 {@code .dal} 的新增依赖都会变红、阻断合入。
|
||||
* 已知违例单点显式登记于 {@link #KNOWN_VIOLATION_EXEMPTIONS} + {@code .agents/rules/bc-boundaries.md §三};
|
||||
* 违例消除后从豁免删除该类即收紧——收紧动作本身就是整改验收。</p>
|
||||
*/
|
||||
class BcBoundaryArchTest {
|
||||
|
||||
@ -23,25 +32,71 @@ class BcBoundaryArchTest {
|
||||
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
|
||||
.importPackages("cn.iocoder.muse.module");
|
||||
|
||||
/** Muse 业务域 BC 集合;规则只在这些域之间强制边界,不约束 yudao 平台模块(system/infra 等)的内部依赖。 */
|
||||
private static final Set<String> BUSINESS_BCS = Set.of(
|
||||
"ai", "content", "knowledge", "market", "meta", "member", "events");
|
||||
|
||||
/**
|
||||
* AI 域不得直连他域 DAL。
|
||||
* 已知违例豁免清单(整改期,单点、显式、可见)。详见 {@code .agents/rules/bc-boundaries.md §三}。
|
||||
*
|
||||
* <p>跨 BC 只能走对外 API/facade-api 契约;任何 AI 类对他域 DAL(DO/Mapper)的依赖都会让本测试变红、阻断合入。
|
||||
* 历史违例 {@code ContentMuseWorkOwnerFacade} 已整改(改消费 content 对外 API {@code MuseContentWorkOwnerApi}),
|
||||
* 故此处**不再设任何豁免**——门禁已收紧(整改记录见 .agents/rules/bc-boundaries.md §三)。</p>
|
||||
* <p>market 的投影 provider/service 仍直写 member 的 account-record-projection 表(经 member.dal Mapper);
|
||||
* 属**写路径**跨 BC 重构(需 member 暴露写端口 + DTO),风险较高,列为单点登记、待专项整改;
|
||||
* 整改后从本清单删除,门禁随即收紧。</p>
|
||||
*/
|
||||
private static final Set<String> KNOWN_VIOLATION_EXEMPTIONS = Set.of(
|
||||
"cn.iocoder.muse.module.market.application.muse.MarketAccountProjectionProvider",
|
||||
"cn.iocoder.muse.module.market.application.muse.AdminMarketReviewServiceImpl",
|
||||
"cn.iocoder.muse.module.market.application.muse.MarketInstallServiceImpl",
|
||||
"cn.iocoder.muse.module.market.application.muse.MarketLicenseServiceImpl",
|
||||
"cn.iocoder.muse.module.market.application.muse.MarketPublishServiceImpl");
|
||||
|
||||
/**
|
||||
* 任一业务 BC 不得直连他域 DAL(通用规则,覆盖全部业务 BC 间方向)。
|
||||
*/
|
||||
@Test
|
||||
void ai_bc_must_not_depend_on_other_bc_dal() {
|
||||
ArchRule rule = noClasses()
|
||||
.that().resideInAPackage("..muse.module.ai..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage(
|
||||
"..muse.module.content.dal..",
|
||||
"..muse.module.knowledge.dal..",
|
||||
"..muse.module.market.dal..",
|
||||
"..muse.module.meta.dal..",
|
||||
"..muse.module.member.dal..")
|
||||
.because("BC 边界:AI 不得直连他域 DAL(DO/Mapper),跨 BC 只能走对外 API/facade-api 契约;"
|
||||
+ "历史违例 ContentMuseWorkOwnerFacade 已整改消除,门禁不再豁免任何 AI 类");
|
||||
void no_business_bc_may_depend_on_another_bc_dal() {
|
||||
ArchRule rule = noClasses().should(new ArchCondition<JavaClass>(
|
||||
"依赖他域 DAL(跨 BC 只能走对外 API / facade-api 契约)") {
|
||||
@Override
|
||||
public void check(JavaClass origin, ConditionEvents events) {
|
||||
String originModule = moduleOf(origin.getPackageName());
|
||||
// 只约束业务 BC;豁免类跳过(已单点登记整改)
|
||||
if (originModule == null || !BUSINESS_BCS.contains(originModule)
|
||||
|| KNOWN_VIOLATION_EXEMPTIONS.contains(origin.getFullName())) {
|
||||
return;
|
||||
}
|
||||
for (Dependency dependency : origin.getDirectDependenciesFromSelf()) {
|
||||
String targetPackage = dependency.getTargetClass().getPackageName();
|
||||
if (!isDalPackage(targetPackage)) {
|
||||
continue;
|
||||
}
|
||||
String targetModule = moduleOf(targetPackage);
|
||||
// 命中:目标是某业务 BC 的 DAL,且与来源不是同一 BC
|
||||
if (targetModule != null && BUSINESS_BCS.contains(targetModule)
|
||||
&& !targetModule.equals(originModule)) {
|
||||
events.add(SimpleConditionEvent.violated(dependency, dependency.getDescription()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).because("BC 边界:任一业务域不得直连他域 DAL(DO/Mapper),跨 BC 只能走对外 API / facade-api 契约;"
|
||||
+ "已知违例见 .agents/rules/bc-boundaries.md §三 单点登记");
|
||||
rule.check(museModuleClasses);
|
||||
}
|
||||
|
||||
/** 从包名 {@code cn.iocoder.muse.module.<X>.*} 提取业务域段 {@code <X>};非该前缀返回 null。 */
|
||||
private static String moduleOf(String packageName) {
|
||||
String prefix = "cn.iocoder.muse.module.";
|
||||
if (!packageName.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
String rest = packageName.substring(prefix.length());
|
||||
int dot = rest.indexOf('.');
|
||||
return dot < 0 ? rest : rest.substring(0, dot);
|
||||
}
|
||||
|
||||
/** 判断包是否属于某域的 DAL 层({@code ..dal} 或 {@code ..dal.. })。 */
|
||||
private static boolean isDalPackage(String packageName) {
|
||||
return packageName.contains(".dal.") || packageName.endsWith(".dal");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user