/** * 三面骨架的唯一事实源(docs/产品设计.md §1 / §6)。 * * 产品分三面:设计中心定义企业、运营中心看企业跑、工作台在企业里干活。 * 区归属、入口亮灯、旧链接改写都从这里出 —— 壳层只消费,不各写各的。 */ import type { GovernanceFunction, Session } from '@contracts'; import { GOV_ZONES } from './gov-route.ts'; import type { GovZone } from './gov-route.ts'; /** 两个治理台面 —— 工作台是第三面,但它不吃这套区/页签骨架 */ export type Surface = 'design' | 'ops'; export const SURFACE_META: Readonly> = { design: { path: '/design', brand: '设计中心', searchHint: '检索定义:流程包 / 活动 / 工具 / 数据域 / 调起规则…', }, ops: { path: '/ops', brand: '运营中心', searchHint: '检索运行:工作流 / 任务 / 员工 / 台账…', }, }; /** 区归属:设计五区 + 运营六区,恰好铺满 `GOV_ZONES`(由测试守住) */ export const SURFACE_ZONES: Readonly> = { design: ['blueprint', 'workflows', 'library', 'tools', 'data', 'testbed'], ops: ['cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system'], }; /** * 亮灯功能集 —— 授权模型不动(仍是九功能 × 读/写/治),这里只说「哪个功能属哪一面」。 * * `trigger` 与 `data` 两面都算:触发器**定义归设计、启停归运营**; * 数据**结构归设计、行数据归运营**。同一功能的两种动作各归其位,不拆授权。 */ export const SURFACE_FNS: Readonly> = { design: ['process', 'activity', 'tool', 'data', 'trigger'], ops: ['task', 'staff', 'memory', 'system', 'trigger', 'data'], }; /** * 台面入口是否亮灯。 * * 判据是「持有该面任一功能的**任意**授权」—— 动作蕴含 govern ⊃ write ⊃ read, * 有 grant 即至少可读,入口就该看得见;进去之后动作仍逐功能校验。 */ export function canSeeSurface(session: Session | undefined, surface: Surface): boolean { if (session === undefined) return false; const fns = SURFACE_FNS[surface]; return session.grants.some((g) => g.object.kind === 'governance' && fns.includes(g.object.function)); } export function surfaceOfZone(zone: GovZone): Surface { return SURFACE_ZONES.design.includes(zone) ? 'design' : 'ops'; } function isGovZone(value: string): value is GovZone { return (GOV_ZONES as readonly string[]).includes(value); } /** * 已删区的改嫁表:键是 `区` 或 `区/页签`,值是新台面下的 `台面/区/页签`。 * 精确到页签的先匹配 —— 记忆的两个旧页签各归其位(度量升驾驶舱,活动记忆归知识库)。 */ const LEGACY_MOVES: Readonly> = { triggers: 'design/workflows/rules', memory: 'ops/cockpit/dash', 'memory/layers': 'ops/knowledge/layers', }; /** 运营首页 —— 认不出的旧链接一律落这儿,不给白屏 */ const OPS_HOME = 'ops/cockpit/dash'; function toUrl(place: string): string { const head = place.split('/')[0]; const surface: Surface = head === 'design' ? 'design' : 'ops'; return `${SURFACE_META[surface].path}#/${place}`; } /** * 旧 `#/gov/<区>/…` 深链 → 新台面的完整 URL(pathname + hash)。 * 非 gov 哈希返回 `undefined`(不是我们的事,别乱改人家的地址)。 */ export function legacyGovTarget(hash: string): string | undefined { const parts = hash .replace(/^#\/?/, '') .split('/') .filter((s) => s !== ''); if (parts[0] !== 'gov') return undefined; const zone = parts[1]; if (zone === undefined) return toUrl(OPS_HOME); const moved = LEGACY_MOVES[`${zone}/${parts[2] ?? ''}`] ?? LEGACY_MOVES[zone]; if (moved !== undefined) return toUrl(moved); if (!isGovZone(zone)) return toUrl(OPS_HOME); // 仍在的区:原样带着页签与下钻栈搬到新台面 return toUrl([surfaceOfZone(zone), ...parts.slice(1)].join('/')); }