M1: 底座 —— @contracts 首版 + event / ledger / org / llm

@contracts 首版(§8.1 九个文件):
- id 品牌 ID / terminal 终态五名与归侧四标签 / envelope 信封六字段与四类事件
- card 工单两模式 + 对话卡族(形状对齐 2026-07-26 原型定稿)
- contract7 契约七件 / ledger 三级寻址与写三分 / process-package 流程包 DSL 与触发器三类
- manifest 活动包清单六区(含 CMMN·DMN·步序声明的资产形状)/ org / api 路由契约

模块(L1):
- event   信封存储、归属解析三级短路、投递五诺、监听注册表、三级消费裁定
- ledger  schema 自生长(最大化合并/最宽类型/只增不破坏)、任务视图叠加、写三分、冲突检查
- org     岗位授权(客体三类×动作三种)、停用前置检查、会话、种子播种
- llm     四元绑定、ModelGateway 三模式(live/record/replay)—— 两缝之一

app 装配:Hono 服务 + 鉴权中间件 + 治理台/工作台路由;drizzle 迁移;AES-256-GCM 凭证密封

ADR-007 工具链与门禁 · ADR-008 投递队列 · ADR-009 凭证加密
(ADR 编号自 007 续:编码规范 §14 已用到 006,README 写「从 005 续」是笔误)

门禁五道全绿;96 个测试通过,含对真实 PG 的 ledger 集成测试

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
JCAI Main Agent
2026-07-27 00:59:14 +08:00
co-authored by Claude Opus 5
parent 7cd664af3a
commit 14d4fa046a
120 changed files with 15236 additions and 78 deletions
+33
View File
@@ -0,0 +1,33 @@
# module: org
职责: 员工与岗位:账号、岗位授权(客体三类 × 动作三种)、停用前置检查、会话装载、
岗位在岗人员查询(负责人四策略解析的候选集来源)、种子数据播种
不负责: 负责人解析算法本身(见 process)、任务与卡的在途判定(经 InFlightWorkProbe 端口由 app 注入)、
鉴权中间件与 cookie 会话(app 层)
公开出口: index.ts
- 类型:`OrgService` `StaffWithPosition` `CreateStaffInput` 与 @contracts 的 `Staff` / `Position` /
`Grant` / `GrantObject` / `GrantAction` / `DisableCheck` / `Session` 转出
- 错误:`StaffNotFoundError` `PositionNotFoundError` `AccountTakenError` `DisableBlockedError`
- 端口类型:`PositionRepository` `StaffRepository` `GrantRepository` `InFlightWorkProbe`
`Clock` `IdGenerator`
- 用例工厂:`createOrgService(deps: OrgServiceDeps): OrgService`
- 纯函数:`isAuthorized` `actionCovers` `objectCovers`
- 常量:`POSITION_SEED` `STAFF_SEED` `DEFAULT_LOGIN_ACCOUNT`
- adapter 工厂(供 app 装配):`createPositionRepository` `createStaffRepository`
`createGrantRepository` `objectKeyOf`
- Drizzle 表(供 app 迁移与 join):`positionTable` `staffTable` `grantTable`
层级: L1(编码规范 §4)
依赖: @contracts;@shared/db(Drizzle 句柄与 sys schema)
port:`InFlightWorkProbe`(app 注入 task / human-tool 的只读用例)、`IdGenerator`
被依赖: process(负责人解析的候选集)、message、interaction、human-tool、report、
app(鉴权中间件与 /api/gov/staff 路由组装)
不变量:
- 动作蕴含:治理 ⊃ 写 ⊃ 读;数据客体三级覆盖(域 ⊃ 对象 ⊃ 字段)
- 停用是全有全无:前置检查未过即抛 `DisableBlockedError`,不做部分执行
- `authorize` 对停用员工恒 false
- `ensureSeeded` 幂等:按 code / account 判存在,已存在则跳过,不覆盖人工改动
影响半径: index/contract/port 未变 → 对外零影响
@@ -0,0 +1,92 @@
/** domain 测试零 mock(编码规范 §16) */
import { describe, expect, it } from 'vitest';
import type { Grant, PositionId } from '@contracts';
import { actionCovers, isAuthorized, objectCovers } from '../internal/domain/authorization.ts';
const pos = 'p-bd' as PositionId;
describe('actionCovers —— 动作蕴含', () => {
it('治理蕴含写与读', () => {
expect(actionCovers('govern', 'write')).toBe(true);
expect(actionCovers('govern', 'read')).toBe(true);
});
it('写蕴含读,但读不蕴含写', () => {
expect(actionCovers('write', 'read')).toBe(true);
expect(actionCovers('read', 'write')).toBe(false);
});
});
describe('objectCovers —— 客体三级覆盖', () => {
it('对象级授权覆盖其下字段', () => {
expect(
objectCovers(
{ kind: 'data', domain: 'shared', object: 'customer' },
{ kind: 'data', domain: 'shared', object: 'customer', field: 'grade' },
),
).toBe(true);
});
it('字段级授权不覆盖别的字段', () => {
expect(
objectCovers(
{ kind: 'data', domain: 'shared', object: 'customer', field: 'grade' },
{ kind: 'data', domain: 'shared', object: 'customer', field: 'payment_terms' },
),
).toBe(false);
});
it('域级通配覆盖域内全部对象', () => {
expect(
objectCovers(
{ kind: 'data', domain: 'shared', object: '*' },
{ kind: 'data', domain: 'shared', object: 'supplier', field: 'contact' },
),
).toBe(true);
});
it('跨域不覆盖', () => {
expect(
objectCovers(
{ kind: 'data', domain: 'shared', object: '*' },
{ kind: 'data', domain: 'quote_intake', object: 'record' },
),
).toBe(false);
});
it('治理功能逐项授权', () => {
expect(objectCovers({ kind: 'governance', function: 'task' }, { kind: 'governance', function: 'task' })).toBe(true);
expect(objectCovers({ kind: 'governance', function: 'task' }, { kind: 'governance', function: 'staff' })).toBe(false);
});
it('任务客体未限定活动即覆盖全部任务', () => {
expect(objectCovers({ kind: 'task' }, { kind: 'task', activityId: 'a1' })).toBe(true);
expect(objectCovers({ kind: 'task', activityId: 'a1' }, { kind: 'task', activityId: 'a2' })).toBe(false);
});
it('客体类型不同一律不覆盖', () => {
expect(objectCovers({ kind: 'task' }, { kind: 'governance', function: 'task' })).toBe(false);
});
});
describe('isAuthorized —— 任一授权覆盖即通过', () => {
const grants: Grant[] = [
{ positionId: pos, object: { kind: 'governance', function: 'task' }, action: 'read' },
{ positionId: pos, object: { kind: 'data', domain: 'shared', object: '*' }, action: 'write' },
];
it('命中数据域的写', () => {
expect(isAuthorized(grants, { kind: 'data', domain: 'shared', object: 'customer' }, 'write')).toBe(true);
});
it('写权蕴含读权', () => {
expect(isAuthorized(grants, { kind: 'data', domain: 'shared', object: 'customer' }, 'read')).toBe(true);
});
it('只读的治理功能不得写', () => {
expect(isAuthorized(grants, { kind: 'governance', function: 'task' }, 'govern')).toBe(false);
});
it('空授权一律不通过', () => {
expect(isAuthorized([], { kind: 'governance', function: 'system' }, 'read')).toBe(false);
});
});
+63
View File
@@ -0,0 +1,63 @@
/**
* module: org —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。
*/
import type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff, StaffId } from '@contracts';
export type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff };
/** 员工连同其岗位的读模型 —— 治理台员工管理列表用 */
export type StaffWithPosition = Staff & { readonly position: Position };
export type CreateStaffInput = {
readonly account: string;
readonly name: string;
readonly positionId: string;
};
export class StaffNotFoundError extends Error {
constructor(readonly staffId: StaffId) {
super(`员工不存在:${staffId}`);
this.name = 'StaffNotFoundError';
}
}
export class PositionNotFoundError extends Error {
constructor(readonly positionCode: string) {
super(`岗位不存在:${positionCode}`);
this.name = 'PositionNotFoundError';
}
}
export class AccountTakenError extends Error {
constructor(readonly account: string) {
super(`账号已存在:${account}`);
this.name = 'AccountTakenError';
}
}
/** 停用前置检查未过:在途任务与待答卡须先改派完毕(OS 02) */
export class DisableBlockedError extends Error {
constructor(readonly check: DisableCheck) {
super(`员工停用前置检查未过:在途任务 ${check.openTasks.length} 件、待答卡 ${check.openCards.length} 张`);
this.name = 'DisableBlockedError';
}
}
export type OrgService = {
listStaff(): Promise<{ staff: StaffWithPosition[]; positions: Position[] }>;
getStaff(staffId: StaffId): Promise<StaffWithPosition>;
findByAccount(account: string): Promise<StaffWithPosition | undefined>;
createStaff(input: CreateStaffInput): Promise<Staff>;
/** 停用前置检查 —— 只读,治理台先看再点 */
checkDisable(staffId: StaffId): Promise<DisableCheck>;
/** 停用 —— 前置检查不过即抛 DisableBlockedError,不做部分执行 */
disableStaff(staffId: StaffId): Promise<void>;
listGrants(positionId: string): Promise<Grant[]>;
setGrants(positionId: string, grants: readonly Grant[]): Promise<Grant[]>;
/** 岗位权限校验 —— app 层鉴权中间件读它 */
authorize(staffId: StaffId, object: GrantObject, action: GrantAction): Promise<boolean>;
loadSession(account: string): Promise<Session | undefined>;
/** 岗位在岗人员 —— 负责人四策略解析的候选集来源(由 process 调用) */
staffOfPosition(positionCode: string): Promise<StaffWithPosition[]>;
ensureSeeded(): Promise<void>;
};
+44
View File
@@ -0,0 +1,44 @@
/**
* module: org —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type {
CreateStaffInput,
DisableCheck,
Grant,
GrantAction,
GrantObject,
OrgService,
Position,
Session,
Staff,
StaffWithPosition,
} from './contract.ts';
export {
AccountTakenError,
DisableBlockedError,
PositionNotFoundError,
StaffNotFoundError,
} from './contract.ts';
export type {
Clock,
GrantRepository,
IdGenerator,
InFlightWorkProbe,
PositionRepository,
StaffRepository,
} from './port.ts';
export type { OrgServiceDeps } from './internal/usecase/org-service.ts';
export { createOrgService } from './internal/usecase/org-service.ts';
export { isAuthorized, actionCovers, objectCovers } from './internal/domain/authorization.ts';
export { DEFAULT_LOGIN_ACCOUNT, POSITION_SEED, STAFF_SEED } from './internal/domain/seed-data.ts';
export {
createGrantRepository,
createPositionRepository,
createStaffRepository,
objectKeyOf,
} from './internal/adapter/org-repository.ts';
export { grantTable, positionTable, staffTable } from './internal/adapter/org-schema.ts';
@@ -0,0 +1,132 @@
/**
* org 的仓储 adapter —— Drizzle 只准出现在 adapter 与 app(编码规范 §5.3 / §11.4)。
*/
import { and, eq } from 'drizzle-orm';
import type { Grant, GrantObject, Position, PositionId, Staff, StaffId } from '@contracts';
import { grantSchema, positionIdSchema, positionSchema, staffSchema } from '@contracts';
import type { Db } from '@shared/db.ts';
import type { GrantRepository, PositionRepository, StaffRepository } from '../../port.ts';
import { grantTable, positionTable, staffTable } from './org-schema.ts';
/** 授权客体的规范化串 —— 主键的一部分,保证同一客体不重复授权 */
export function objectKeyOf(object: GrantObject): string {
switch (object.kind) {
case 'data':
return `data:${object.domain}.${object.object}${object.field === undefined ? '' : `.${object.field}`}`;
case 'governance':
return `governance:${object.function}`;
case 'task':
return `task${object.activityId === undefined ? '' : `:${object.activityId}`}`;
}
}
export function createPositionRepository(db: Db): PositionRepository {
const toPosition = (row: typeof positionTable.$inferSelect): Position =>
positionSchema.parse({
id: row.id,
code: row.code,
name: row.name,
...(row.note === null ? {} : { note: row.note }),
});
return {
async list() {
const rows = await db.select().from(positionTable).orderBy(positionTable.code);
return rows.map(toPosition);
},
async findByCode(code) {
const rows = await db.select().from(positionTable).where(eq(positionTable.code, code)).limit(1);
return rows[0] === undefined ? undefined : toPosition(rows[0]);
},
async findById(id) {
const rows = await db.select().from(positionTable).where(eq(positionTable.id, id)).limit(1);
return rows[0] === undefined ? undefined : toPosition(rows[0]);
},
async upsert(position) {
const id = position.id ?? positionIdSchema.parse(position.code.toLowerCase());
const [row] = await db
.insert(positionTable)
.values({ id, code: position.code, name: position.name, note: position.note ?? null })
.onConflictDoUpdate({
target: positionTable.code,
set: { name: position.name, note: position.note ?? null },
})
.returning();
if (row === undefined) throw new Error('岗位写入未返回行');
return toPosition(row);
},
};
}
export function createStaffRepository(db: Db): StaffRepository {
const toStaff = (row: typeof staffTable.$inferSelect): Staff =>
staffSchema.parse({
id: row.id,
account: row.account,
name: row.name,
positionId: row.positionId,
state: row.state,
});
return {
async list() {
const rows = await db.select().from(staffTable).orderBy(staffTable.account);
return rows.map(toStaff);
},
async findById(id: StaffId) {
const rows = await db.select().from(staffTable).where(eq(staffTable.id, id)).limit(1);
return rows[0] === undefined ? undefined : toStaff(rows[0]);
},
async findByAccount(account: string) {
const rows = await db.select().from(staffTable).where(eq(staffTable.account, account)).limit(1);
return rows[0] === undefined ? undefined : toStaff(rows[0]);
},
async listByPosition(positionId: PositionId) {
const rows = await db.select().from(staffTable).where(eq(staffTable.positionId, positionId));
return rows.map(toStaff);
},
async insert(staff: Staff) {
const [row] = await db.insert(staffTable).values(staff).returning();
if (row === undefined) throw new Error('员工写入未返回行');
return toStaff(row);
},
async setState(id: StaffId, state) {
await db.update(staffTable).set({ state }).where(eq(staffTable.id, id));
},
};
}
export function createGrantRepository(db: Db): GrantRepository {
const toGrant = (row: typeof grantTable.$inferSelect): Grant =>
grantSchema.parse({ positionId: row.positionId, object: row.object, action: row.action });
return {
async listByPosition(positionId: PositionId) {
const rows = await db
.select()
.from(grantTable)
.where(and(eq(grantTable.positionId, positionId), eq(grantTable.enabled, true)));
return rows.map(toGrant);
},
async replaceForPosition(positionId: PositionId, grants) {
return db.transaction(async (tx) => {
await tx.delete(grantTable).where(eq(grantTable.positionId, positionId));
if (grants.length === 0) return [];
const rows = await tx
.insert(grantTable)
.values(
grants.map((g) => ({
positionId,
objectKey: objectKeyOf(g.object),
action: g.action,
object: g.object,
enabled: true,
})),
)
.onConflictDoNothing()
.returning();
return rows.map(toGrant);
});
},
};
}
@@ -0,0 +1,48 @@
/**
* org 的 sys schema 表(编码规范 §7 / ADR-002:sys 由各模块 adapter 经 Drizzle 单写)。
* drizzle-kit 以 glob 收集本仓全部 `*-schema.ts`(见 drizzle.config.ts)。
*/
import { boolean, index, jsonb, primaryKey, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sys } from '@shared/db.ts';
export const positionTable = sys.table(
'position',
{
id: text('id').primaryKey(),
code: text('code').notNull(),
name: text('name').notNull(),
note: text('note'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [uniqueIndex('position_code_key').on(t.code)],
);
export const staffTable = sys.table(
'staff',
{
id: text('id').primaryKey(),
account: text('account').notNull(),
name: text('name').notNull(),
positionId: text('position_id')
.notNull()
.references(() => positionTable.id),
state: text('state').notNull().default('active'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [uniqueIndex('staff_account_key').on(t.account), index('staff_position_idx').on(t.positionId)],
);
export const grantTable = sys.table(
'position_grant',
{
positionId: text('position_id')
.notNull()
.references(() => positionTable.id, { onDelete: 'cascade' }),
/** 客体的规范化串:data:<域>.<对象>[.<字段>] / governance:<功能> / task[:<活动>] */
objectKey: text('object_key').notNull(),
action: text('action').notNull(),
object: jsonb('object').notNull(),
enabled: boolean('enabled').notNull().default(true),
},
(t) => [primaryKey({ columns: [t.positionId, t.objectKey, t.action] })],
);
@@ -0,0 +1,56 @@
/**
* 岗位授权判定 —— 纯逻辑,零 IO(编码规范 §5.3)。
*
* 权限按岗位授予:客体三类(数据 域.对象[.字段] / 治理功能 / 任务)× 动作三种(读 / 写 / 治理)。
* 语义出处:OS 02 员工管理。
*/
import type { Grant, GrantAction, GrantObject } from '@contracts';
/** 动作蕴含:治理 ⊃ 写 ⊃ 读 —— 有写权即可读,有治理权即可写 */
const IMPLIES: Readonly<Record<GrantAction, readonly GrantAction[]>> = {
govern: ['govern', 'write', 'read'],
write: ['write', 'read'],
read: ['read'],
};
export function actionCovers(granted: GrantAction, required: GrantAction): boolean {
return IMPLIES[granted].includes(required);
}
/**
* 客体覆盖:授权客体是否覆盖被请求客体。
* 数据客体分三级:域.对象.字段 —— 授到域即覆盖其下全部对象,授到对象即覆盖其下全部字段。
*/
export function objectCovers(granted: GrantObject, required: GrantObject): boolean {
if (granted.kind !== required.kind) return false;
switch (granted.kind) {
case 'data': {
const req = required as Extract<GrantObject, { kind: 'data' }>;
if (granted.domain !== req.domain) return false;
// 授权到域级(未指定对象名的通配 '*')
if (granted.object !== '*' && granted.object !== req.object) return false;
if (granted.field === undefined) return true;
return granted.field === req.field;
}
case 'governance': {
const req = required as Extract<GrantObject, { kind: 'governance' }>;
return granted.function === req.function;
}
case 'task': {
const req = required as Extract<GrantObject, { kind: 'task' }>;
// 未限定活动 = 覆盖全部任务
if (granted.activityId === undefined) return true;
return granted.activityId === req.activityId;
}
}
}
/** 任一授权覆盖即通过 */
export function isAuthorized(
grants: readonly Grant[],
object: GrantObject,
action: GrantAction,
): boolean {
return grants.some((g) => actionCovers(g.action, action) && objectCovers(g.object, object));
}
@@ -0,0 +1,25 @@
/**
* 停用前置检查 —— 纯逻辑,零 IO(编码规范 §5.3)。
*
* OS 02:员工停用前,其在途任务与待答卡须先改派完毕。
*/
import type { DisableCheck, StaffId } from '@contracts';
export type InFlightWork = {
readonly openTasks: ReadonlyArray<{ taskId: string; title: string }>;
readonly openCards: ReadonlyArray<{ cardId: string; title: string }>;
readonly openApprovals: ReadonlyArray<{ approvalId: string; title: string }>;
};
export function buildDisableCheck(staffId: StaffId, work: InFlightWork): DisableCheck {
const openTasks = [...work.openTasks];
const openCards = [...work.openCards];
const openApprovals = [...work.openApprovals];
return {
staffId,
blocked: openTasks.length > 0 || openCards.length > 0 || openApprovals.length > 0,
openTasks,
openCards,
openApprovals,
};
}
@@ -0,0 +1,61 @@
/**
* 种子数据(README《种子数据》)—— 纯常量,零 IO。
* 部署边界:单次部署 ≤50 人、单组织、不做多租户(OS 0.3)。
*/
export const POSITION_SEED = [
{ code: 'PM', name: '项目经理', note: '定价审批、策略拍板' },
{ code: 'BD', name: '商务', note: 'RFQ 受理、成本定价、报价议价、情报归档' },
{ code: 'FAE', name: '现场应用', note: '替代选型' },
{ code: 'PRM', name: '采购/寻源', note: '渠道寻源' },
{ code: 'OP', name: '运营', note: '运营支持' },
] as const;
export const STAFF_SEED = [
{ account: 'dingziqiao', name: '丁子乔', position: 'PM' },
{ account: 'fengbing', name: '冯兵', position: 'BD' },
{ account: 'wangziming', name: '王梓名', position: 'FAE' },
{ account: 'linpeixuan', name: '林佩萱', position: 'PRM' },
{ account: 'liyuanchu', name: '李元初', position: 'OP' },
] as const;
/** 登录者 = 冯兵(BD);MVP 单组织 cookie session(编码规范 §4 鉴权定调) */
export const DEFAULT_LOGIN_ACCOUNT = 'fengbing';
export type PositionSeed = (typeof POSITION_SEED)[number];
export type StaffSeed = (typeof STAFF_SEED)[number];
/**
* 岗位默认授权 —— 治理台九功能对 PM 全开,其余岗位只开与其职责相关的读。
* 任务负责人与被征答人对所涉内容**天然可见**,不靠这里授权(OS 02)。
*/
export const POSITION_GRANT_SEED: Readonly<Record<string, ReadonlyArray<{ fn: string; action: 'read' | 'write' | 'govern' }>>> = {
PM: [
{ fn: 'task', action: 'govern' },
{ fn: 'staff', action: 'govern' },
{ fn: 'process', action: 'govern' },
{ fn: 'trigger', action: 'govern' },
{ fn: 'activity', action: 'govern' },
{ fn: 'tool', action: 'govern' },
{ fn: 'data', action: 'govern' },
{ fn: 'memory', action: 'govern' },
{ fn: 'system', action: 'govern' },
],
BD: [
{ fn: 'task', action: 'read' },
{ fn: 'data', action: 'read' },
],
FAE: [
{ fn: 'task', action: 'read' },
{ fn: 'data', action: 'read' },
],
PRM: [
{ fn: 'task', action: 'read' },
{ fn: 'data', action: 'read' },
],
OP: [
{ fn: 'task', action: 'read' },
{ fn: 'data', action: 'read' },
{ fn: 'memory', action: 'read' },
],
};
@@ -0,0 +1,183 @@
/**
* org 用例 —— 编排 domain,依赖 port.ts 声明的能力,不 import 任何 adapter(编码规范 §5.3)。
*/
import type { Grant, GrantAction, GrantObject, Position, PositionId, Session, Staff, StaffId } from '@contracts';
import { governanceFunctionSchema, positionIdSchema, staffIdSchema } from '@contracts';
import type {
CreateStaffInput,
OrgService,
StaffWithPosition,
} from '../../contract.ts';
import {
AccountTakenError,
DisableBlockedError,
PositionNotFoundError,
StaffNotFoundError,
} from '../../contract.ts';
import type {
GrantRepository,
IdGenerator,
InFlightWorkProbe,
PositionRepository,
StaffRepository,
} from '../../port.ts';
import { isAuthorized } from '../domain/authorization.ts';
import { buildDisableCheck } from '../domain/disable-check.ts';
import { POSITION_GRANT_SEED, POSITION_SEED, STAFF_SEED } from '../domain/seed-data.ts';
export type OrgServiceDeps = {
readonly positions: PositionRepository;
readonly staff: StaffRepository;
readonly grants: GrantRepository;
readonly inFlight: InFlightWorkProbe;
readonly ids: IdGenerator;
};
export function createOrgService(deps: OrgServiceDeps): OrgService {
async function positionMap(): Promise<Map<string, Position>> {
const all = await deps.positions.list();
return new Map(all.map((p) => [p.id, p]));
}
async function withPosition(staff: Staff, positions?: Map<string, Position>): Promise<StaffWithPosition> {
const map = positions ?? (await positionMap());
const position = map.get(staff.positionId);
if (!position) throw new PositionNotFoundError(staff.positionId);
return { ...staff, position };
}
async function grantsOfStaff(staffId: StaffId): Promise<Grant[]> {
const staff = await deps.staff.findById(staffId);
if (!staff) throw new StaffNotFoundError(staffId);
return deps.grants.listByPosition(staff.positionId);
}
return {
async listStaff() {
const [rows, positions] = await Promise.all([deps.staff.list(), deps.positions.list()]);
const map = new Map(positions.map((p) => [p.id, p]));
const staff = await Promise.all(rows.map((s) => withPosition(s, map)));
return { staff, positions };
},
async getStaff(staffId) {
const staff = await deps.staff.findById(staffId);
if (!staff) throw new StaffNotFoundError(staffId);
return withPosition(staff);
},
async findByAccount(account) {
const staff = await deps.staff.findByAccount(account);
if (!staff) return undefined;
return withPosition(staff);
},
async createStaff(input: CreateStaffInput) {
const taken = await deps.staff.findByAccount(input.account);
if (taken) throw new AccountTakenError(input.account);
const positionId = positionIdSchema.parse(input.positionId);
const position = await deps.positions.findById(positionId);
if (!position) throw new PositionNotFoundError(input.positionId);
return deps.staff.insert({
id: staffIdSchema.parse(deps.ids.next()),
account: input.account,
name: input.name,
positionId,
state: 'active',
});
},
async checkDisable(staffId) {
const staff = await deps.staff.findById(staffId);
if (!staff) throw new StaffNotFoundError(staffId);
const [openTasks, openCards, openApprovals] = await Promise.all([
deps.inFlight.openTasksOf(staffId),
deps.inFlight.openCardsOf(staffId),
deps.inFlight.openApprovalsOf(staffId),
]);
return buildDisableCheck(staffId, { openTasks, openCards, openApprovals });
},
async disableStaff(staffId) {
const check = await this.checkDisable(staffId);
if (check.blocked) throw new DisableBlockedError(check);
await deps.staff.setState(staffId, 'disabled');
},
async listGrants(positionId) {
return deps.grants.listByPosition(positionIdSchema.parse(positionId));
},
async setGrants(positionId, grants) {
return deps.grants.replaceForPosition(positionIdSchema.parse(positionId), grants);
},
async authorize(staffId: StaffId, object: GrantObject, action: GrantAction) {
const staff = await deps.staff.findById(staffId);
if (!staff || staff.state !== 'active') return false;
const grants = await grantsOfStaff(staffId);
return isAuthorized(grants, object, action);
},
async loadSession(account): Promise<Session | undefined> {
const staff = await deps.staff.findByAccount(account);
if (!staff || staff.state !== 'active') return undefined;
const position = await deps.positions.findById(staff.positionId);
if (!position) throw new PositionNotFoundError(staff.positionId);
const grants = await deps.grants.listByPosition(staff.positionId);
return { staff, position, grants };
},
async staffOfPosition(positionCode) {
const position = await deps.positions.findByCode(positionCode);
if (!position) throw new PositionNotFoundError(positionCode);
const rows = await deps.staff.listByPosition(position.id);
return rows.filter((s) => s.state === 'active').map((s) => ({ ...s, position }));
},
async ensureSeeded() {
const byCode = new Map<string, PositionId>();
for (const seed of POSITION_SEED) {
const existing = await deps.positions.findByCode(seed.code);
const position = existing
? existing
: await deps.positions.upsert({
id: positionIdSchema.parse(deps.ids.next()),
code: seed.code,
name: seed.name,
note: seed.note,
});
byCode.set(seed.code, position.id);
}
for (const seed of STAFF_SEED) {
const existing = await deps.staff.findByAccount(seed.account);
if (existing) continue;
const positionId = byCode.get(seed.position);
if (!positionId) throw new PositionNotFoundError(seed.position);
await deps.staff.insert({
id: staffIdSchema.parse(deps.ids.next()),
account: seed.account,
name: seed.name,
positionId,
state: 'active',
});
}
for (const [code, entries] of Object.entries(POSITION_GRANT_SEED)) {
const positionId = byCode.get(code);
if (!positionId) continue;
const existing = await deps.grants.listByPosition(positionId);
if (existing.length > 0) continue;
await deps.grants.replaceForPosition(
positionId,
entries.map((e) => ({
positionId,
object: { kind: 'governance' as const, function: governanceFunctionSchema.parse(e.fn) },
action: e.action,
})),
);
}
},
};
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "@modules/org",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": { ".": "./index.ts" }
}
+41
View File
@@ -0,0 +1,41 @@
/**
* module: org —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
* adapter 实现之,usecase 依赖之,app 装配之。
*/
import type { Grant, Position, PositionId, Staff, StaffId } from '@contracts';
export type Clock = { now(): Date };
export type PositionRepository = {
list(): Promise<Position[]>;
findByCode(code: string): Promise<Position | undefined>;
findById(id: PositionId): Promise<Position | undefined>;
upsert(position: Omit<Position, 'id'> & { id?: PositionId }): Promise<Position>;
};
export type StaffRepository = {
list(): Promise<Staff[]>;
findById(id: StaffId): Promise<Staff | undefined>;
findByAccount(account: string): Promise<Staff | undefined>;
listByPosition(positionId: PositionId): Promise<Staff[]>;
insert(staff: Staff): Promise<Staff>;
setState(id: StaffId, state: Staff['state']): Promise<void>;
};
export type GrantRepository = {
listByPosition(positionId: PositionId): Promise<Grant[]>;
replaceForPosition(positionId: PositionId, grants: readonly Grant[]): Promise<Grant[]>;
};
/**
* 在途工作探针 —— 停用前置检查要问的是 L6 `task` 与 L3 `human-tool`,
* 二者层级高于 org,故一律经本端口由 app 注入(编码规范 §5.2 同层/跨层协作规矩)。
*/
export type InFlightWorkProbe = {
openTasksOf(staffId: StaffId): Promise<Array<{ taskId: string; title: string }>>;
openCardsOf(staffId: StaffId): Promise<Array<{ cardId: string; title: string }>>;
openApprovalsOf(staffId: StaffId): Promise<Array<{ approvalId: string; title: string }>>;
};
/** ID 生成 —— domain 零随机(§5.3) */
export type IdGenerator = { next(): string };