docs(adr): member-group ADR 改号 0028→0038,避免与 agent-config-folder-tree 撞号

This commit is contained in:
2026-08-06 00:58:51 +08:00
parent ecc92c9a87
commit c96ea60482
13 changed files with 29 additions and 48 deletions
+2 -2
View File
@@ -89,7 +89,7 @@ export interface GroupSearchResult {
}
/** 成员组(ADR-0028);后端返回扁平列表,前端按 parentId/depth 拼树。 */
/** 成员组(ADR-0038);后端返回扁平列表,前端按 parentId/depth 拼树。 */
export interface MemberGroupNode {
readonly id: string;
readonly parentId: string | null;
@@ -97,7 +97,7 @@ export interface MemberGroupNode {
readonly description: string | null;
readonly depth: number;
readonly memberCount: number;
/** 软删标记(ADR-0028 决策4)。null = 活跃;非 null = 已归档,不贡献任何权限。
/** 软删标记(ADR-0038 决策4)。null = 活跃;非 null = 已归档,不贡献任何权限。
* 仅在 ?includeArchived=1 时可能非 null。ISO 串(后端 JSON 序列化后不再是 Date)。 */
readonly archivedAt: string | null;
}
+6 -6
View File
@@ -85,7 +85,7 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真
| `routes/databaseRoutes.ts` | `/database/config``/database/api/stats`、dev 旁路 + 各子路由装配点 |
| `routes/filelibRoutes.ts` | 文件库 树/授权 API |
| `routes/fileRoutes.ts` | 文件库 文件内容/导出 API |
| `routes/memberGroupRoutes.ts` | 成员组管理 API + `/groups/search` + `/users/search`(ADR-0028) |
| `routes/memberGroupRoutes.ts` | 成员组管理 API + `/groups/search` + `/users/search`(ADR-0038) |
| `routes/teacherApp.ts` | `/database/api/login-info` + 老师端 DEV 一键登录 |
| `static.ts` | filelib-web 构建产物托管:`/_filelib/*` 资源 + `/app``/database` 两个 SPA 回退 |
| `filelib/` | 文件库领域层(见下) |
@@ -99,7 +99,7 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真
独立文件库模块。代码注释里的 C/D 编号(契约 8.1、C2、C4、D11D19 等)
出自两份已删除的文档:《文件库-接口契约.md》与 `.omo/文件库-开工计划.md`,
内容可从 git 历史取回。其中 D19(网站管理员 = silo org OWNER/ADMIN)
另见 ADR-0028。**与 hub 自己的 Folder/Project(ADR-0021 explorer)是
另见 ADR-0038。**与 hub 自己的 Folder/Project(ADR-0021 explorer)是
两套体系,不复用。**
| 文件 | 职责 |
@@ -112,9 +112,9 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真
| `filelib/exportService.ts` | 导出 job 状态机(D10 异步)+ ExportAdapter port |
| `filelib/versionStore.ts` | 契约 C1 port + 内存实现(**仅测试用**,ADR-0030) |
| `filelib/gitVersionStore.ts` | **生产** C1 实现:一项目一 git 仓库,VersionId = commit hash(ADR-0030) |
| `filelib/groupResolver.ts` | 契约 C2 port(+ 已弃用的 Team 过渡实现,ADR-0028) |
| `filelib/memberGroupResolver.ts` | **默认** C2 实现:读 in-hub MemberGroup 闭包(ADR-0028) |
| `filelib/memberGroupService.ts` | 成员组 CRUD(含改名)+ 成员增删 + 闭包维护 + 搜索(ADR-0028) |
| `filelib/groupResolver.ts` | 契约 C2 port(+ 已弃用的 Team 过渡实现,ADR-0038) |
| `filelib/memberGroupResolver.ts` | **默认** C2 实现:读 in-hub MemberGroup 闭包(ADR-0038) |
| `filelib/memberGroupService.ts` | 成员组 CRUD(含改名)+ 成员增删 + 闭包维护 + 搜索(ADR-0038) |
| `filelib/groupResolverHttp.ts` | C2 HTTP 实现(HUB_GROUP_SERVICE_URL 启用;失败 → 503) |
| `filelib/audit.ts` | 审计动作词表(C3 §6.3)+ 同事务写入 |
| `filelib/guards.ts` | session → FileLibActor;网站管理员 = org OWNER/ADMIN(D19) |
@@ -124,7 +124,7 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真
- `HUB_FILELIB_STORAGE_ROOT` — 项目 git 仓库根目录(默认 `./.filelib-repos`)
- `HUB_GROUP_SERVICE_URL` — 外部 Group 服务地址(C2);**未配置时读 in-hub
MemberGroup 闭包**(ADR-0028 起的默认;此前是扁平 hub Team)
MemberGroup 闭包**(ADR-0038 起的默认;此前是扁平 hub Team)
## 存储布局(ADR-0030)
+1 -1
View File
@@ -36,7 +36,7 @@ export const FILE_LIB_AUDIT_ACTIONS = {
fileConflictDetected: "file.conflict_detected",
exportRun: "export.run",
adminForceAdjust: "admin.force_adjust",
// ADR-0028:成员组内置进 hub,组动作在本地审计(契约 C3 §6.3 原委托外部 Group 服务)。
// ADR-0038:成员组内置进 hub,组动作在本地审计(契约 C3 §6.3 原委托外部 Group 服务)。
groupCreate: "group.create",
groupUpdate: "group.update",
groupDelete: "group.delete",
+2 -2
View File
@@ -2,7 +2,7 @@
* GroupResolver port(契约 C2)。
*
* 权限计算只依赖这一个查询:"用户 → 所属 Group(含全部祖先)"。
* ADR-0028 起,默认实现是 in-hub 的 MemberGroup 闭包读取器
* ADR-0038 起,默认实现是 in-hub 的 MemberGroup 闭包读取器
* (`createMemberGroupResolver`,见 memberGroupResolver.ts);
* `HUB_GROUP_SERVICE_URL` 配置后切外部 HTTP 实现(groupResolverHttp.ts)。
* 调用方只依赖此 port,不换调用点。
@@ -15,7 +15,7 @@ export interface GroupResolver {
}
/**
* @deprecated ADR-0028:成员组已内置为 in-hub MemberGroup,默认 resolver 改为
* @deprecated ADR-0038:成员组已内置为 in-hub MemberGroup,默认 resolver 改为
* `createMemberGroupResolver`。此扁平 Team 过渡实现不再接线,保留仅为历史参照
* (以及潜在的迁移对照),新代码不要使用。
*
@@ -1,5 +1,5 @@
/**
* 默认 GroupResolver 实现:读 in-hub MemberGroup 闭包(ADR-0028)。
* 默认 GroupResolver 实现:读 in-hub MemberGroup 闭包(ADR-0038)。
*
* resolveMemberGroupIds(user) = 用户**活跃直接组 ∪ 这些组的活跃祖先**,去重
* (闭包 depth0 自身行令每个直接组也是自己的祖先)。等价于:授权放在组 G 上,
+8 -27
View File
@@ -1,5 +1,5 @@
/**
* 成员组(MemberGroup)管理服务(ADR-0028)。
* 成员组(MemberGroup)管理服务(ADR-0038)。
*
* 语义锚定:
* - 全局主体:MemberGroup 无 organizationId,不做租户 scope;审计行挂 silo org
@@ -84,14 +84,14 @@ type Tx = Prisma.TransactionClient;
/* ---------------------------------------------------------------- 内部工具 */
/** 管理门禁:非网站管理员一律 403(决策2)。 */
// 管理门禁非网站管理员一律 403
function requireAdmin(actor: FileLibActor): void {
if (!actor.isWebsiteAdmin) {
throw new FileLibError(403, "forbidden", "group management requires website administrator");
}
}
/** 组名校验(Group 域与节点域分开:轻量 trim/非空/长度,不套用节点命名规则)。 */
// 组名校验
function normalizeGroupName(raw: string): string {
const name = raw.trim();
if (name === "") throw new FileLibError(400, "invalid_request", "group name must not be empty");
@@ -111,7 +111,7 @@ async function requireActiveGroup(
return group;
}
/** 全局用户解析:按 userId,User.feishuOpenId(全局 @unique)。不要求 org 成员。 */
// 全局用户解析按 userId 或 feishuOpenId。不要求 org 成员。
async function resolveUser(
tx: Tx,
input: AddMemberInput,
@@ -211,10 +211,7 @@ export async function createMemberGroup(
});
}
/**
* 改名 / 改描述(决策6)。仅网站管理员。**不动 parentId** —— reparent 仍属 v1
* 范围外(决策5),闭包无需维护。字段缺省即不动;description 传 "" 清空。
*/
/** 改名/改描述(决策6)。不动 parentId(决策5)。 */
export async function updateMemberGroup(
deps: MemberGroupServiceDeps,
actor: FileLibActor,
@@ -306,15 +303,7 @@ export async function deleteMemberGroup(
});
}
/**
* 恢复(取消归档)。仅网站管理员。**与删除不对称**(决策7):
* - 删除级联整棵子树;恢复只恢复「该组 + 其全部已归档祖先」,**不动子树**。
* - 恢复祖先链是必须的:活跃组的祖先必须活跃,否则该组在树上无路径、
* depth 推导(闭包行数)与"祖先必活跃"的前提脱节。
* - 子树保持归档、仍可见(带标记),由管理员逐个决定是否恢复 —— 避免一次
* 恢复意外把整支历史组全部重新授权。
* 恢复即刻恢复该组贡献的权限(实时解析,不缓存)。
*/
/** 恢复(取消归档)。与删除不对称(决策7):只恢复本组+已归档祖先,不动子树。 */
export async function restoreMemberGroup(
deps: MemberGroupServiceDeps,
actor: FileLibActor,
@@ -402,10 +391,7 @@ export async function listMemberGroups(
}));
}
/**
* 组成员列表(仅网站管理员)。**已归档组也可读**(决策7):软删是打标,成员行仍在,
* 后台需要看得见「这个组曾经有谁」。写操作(add/remove)仍要求活跃组 —— 可读不可改。
*/
/** 组成员列表。已归档组也可读(决策7)。 */
export async function listMembers(
deps: MemberGroupServiceDeps,
actor: FileLibActor,
@@ -506,12 +492,7 @@ export async function removeMember(
});
}
/**
* 成员选择器:按显示名/openId 搜全局用户。**仅网站管理员**(与加成员同权,决策2)
* —— 加成员本就能指定任意全局用户(resolveUser 不要求 org 成员),故此端点不扩大
* 已有能力面,只是把"盲敲 id"变成"搜索选择"。
* excludeGroupId 给定时,过滤掉该组的活跃成员(避免选中必然 409 的人)。
*/
/** 成员选择器:按显示名/openId 搜全局用户(决策2)。 */
export async function searchUsers(
deps: MemberGroupServiceDeps,
actor: FileLibActor,
+1 -1
View File
@@ -138,7 +138,7 @@ export async function registerDatabaseRoutes(
// 文件库(独立模块,《文件库-接口契约.md》):API + 老师端 /app 静态托管。
// 依赖装配:VersionStore 是真 git —— 一项目一仓库 <storageRoot>/<nodeId>,
// VersionId = commit hash(ADR-0030);
// GroupResolver 默认读 in-hub MemberGroup 闭包(ADR-0028),
// GroupResolver 默认读 in-hub MemberGroup 闭包(ADR-0038),
// HUB_GROUP_SERVICE_URL 配置后切 HTTP(C2);
// 导出适配器当前为 manifest stub(OPEN-6,真导出工具到位后替换)。
const siloOrg = await config.prisma.organization.findUnique({
+1 -1
View File
@@ -254,7 +254,7 @@ export async function registerFileLibRoutes(
});
// Group 搜索(C2 /groups/search)已迁至 memberGroupRoutes.ts,读 in-hub
// MemberGroup 闭包(ADR-0028)。此处不再注册,避免重复。
// MemberGroup 闭包(ADR-0038)。此处不再注册,避免重复。
}
function parseGrants(raw: unknown): InitialGrant[] | undefined {
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* /database/api/groups/* 成员组管理端点(ADR-0028)。
* /database/api/groups/* 成员组管理端点(ADR-0038)。
* 约定:绝对路径;actorOrNull 前置 fail closed;业务全走 memberGroupService;
* 错误统一 sendRouteError。
*
@@ -108,7 +108,7 @@ export async function registerMemberGroupRoutes(
const { id } = request.params as { id: string };
const body = bodyObject(request.body);
if (body["parentId"] !== undefined) {
throw new FileLibError(400, "invalid_request", "reparent is not supported (ADR-0028)");
throw new FileLibError(400, "invalid_request", "reparent is not supported (ADR-0038)");
}
// description 需区分"未传"(不动)与 ""(清空),故不用 optionalString
// (它把 "" 也归为 undefined)。
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* 成员组(MemberGroup)集成测试(真实 Postgres)。ADR-0028。
* 成员组(MemberGroup)集成测试(真实 Postgres)。ADR-0038。
* 覆盖:嵌套创建 + 闭包维护、解析(直接组 ∪ 活跃祖先)、祖先授权递归传递
* (3.2)、级联软删 + 实时失效、非管理员 403、成员增删幂等/重加、搜索 breadcrumb。
* 运行前提:本地 PG(paradigm:paradigm@127.0.0.1:5432/cph_hub_test)且已 migrate。
@@ -128,7 +128,7 @@ describe("memberGroupService · 改名/改描述(决策6)", () => {
expect(updated.parentId).toBe(a);
expect(updated.depth).toBe(1);
// 闭包逐行未变 —— rename 不碰层级(ADR-0028 决策6 的核心不变量)。
// 闭包逐行未变 —— rename 不碰层级(ADR-0038 决策6 的核心不变量)。
const after = await prisma.memberGroupClosure.findMany({ orderBy: [{ ancestorId: "asc" }, { descendantId: "asc" }] });
expect(after).toEqual(before);
// C 仍在 B 之下,depth 不变。