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:
co-authored by
Claude Opus 5
parent
7cd664af3a
commit
14d4fa046a
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* ledger 用例 —— 编排 domain,依赖 port.ts,不 import 任何 adapter(编码规范 §5.3)。
|
||||
*
|
||||
* 写三分的不变量在此落实(OS 09):
|
||||
* systemWrite → 正式版 + **即时**数据事件
|
||||
* draftWrite → 草稿层,**零**事件
|
||||
* releaseWrite → 冲突检查 → 原子合并 → **此刻**抛事件
|
||||
*/
|
||||
import type { EnvelopeDraft, LedgerPage, LedgerQuery, ObjectDeclaration, TaskId } from '@contracts';
|
||||
import { eventIdSchema } from '@contracts';
|
||||
import type {
|
||||
DraftWrite,
|
||||
LedgerService,
|
||||
ObjectAddress,
|
||||
ReleaseWriteResult,
|
||||
SchemaApplyResult,
|
||||
SystemWrite,
|
||||
} from '../../contract.ts';
|
||||
import { DraftConflictError, ObjectNotMaterializedError } from '../../contract.ts';
|
||||
import type { Clock, DraftStore, EventPublisher, IdGenerator, LedgerStore, ObjectRef } from '../../port.ts';
|
||||
import { mergeDeclarations, planIsNonDestructive, planSchemaChanges } from '../domain/schema-merge.ts';
|
||||
import { detectConflicts, keyOf, overlay, pickKey } from '../domain/task-view.ts';
|
||||
|
||||
export type LedgerServiceDeps = {
|
||||
readonly store: LedgerStore;
|
||||
readonly drafts: DraftStore;
|
||||
readonly events: EventPublisher;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
function groupDeclarations(declarations: readonly ObjectDeclaration[]): Map<string, ObjectDeclaration[]> {
|
||||
const groups = new Map<string, ObjectDeclaration[]>();
|
||||
for (const d of declarations) {
|
||||
const key = `${d.domain}.${d.object}`;
|
||||
const bucket = groups.get(key);
|
||||
if (bucket === undefined) groups.set(key, [d]);
|
||||
else bucket.push(d);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
function dataEvent(
|
||||
ref: ObjectRef,
|
||||
key: Record<string, string | number>,
|
||||
changed: readonly string[],
|
||||
operation: 'insert' | 'update' | 'delete',
|
||||
taskId: TaskId | undefined,
|
||||
cause: { kind: 'ledger-write'; ref: string },
|
||||
): EnvelopeDraft {
|
||||
return {
|
||||
id: eventIdSchema.parse(deps.ids.next()),
|
||||
type: 'data',
|
||||
correlation: {
|
||||
// 未绑任务的系统写不指定消费目标 —— 三级消费由 `event` 判定(OS 6.3)
|
||||
...(taskId === undefined ? {} : { target: { kind: 'task' as const, id: taskId } }),
|
||||
cause,
|
||||
keys: Object.fromEntries(Object.entries(key).map(([k, v]) => [k, String(v)])),
|
||||
},
|
||||
payload: {
|
||||
data: {
|
||||
domain: ref.domain,
|
||||
object: ref.object,
|
||||
primaryKey: key,
|
||||
changed: [...changed],
|
||||
operation,
|
||||
},
|
||||
},
|
||||
occurredAt: deps.clock.now().toISOString(),
|
||||
source: { kind: 'engine', detail: 'ledger' },
|
||||
};
|
||||
}
|
||||
|
||||
async function requireSchema(address: ObjectAddress) {
|
||||
const described = await deps.store.describe(address);
|
||||
if (described === undefined) throw new ObjectNotMaterializedError(`${address.domain}.${address.object}`);
|
||||
return described;
|
||||
}
|
||||
|
||||
return {
|
||||
async applyDeclarations(declarations) {
|
||||
const results: SchemaApplyResult[] = [];
|
||||
for (const [, group] of groupDeclarations(declarations)) {
|
||||
const first = group[0]!;
|
||||
const ref: ObjectRef = { domain: first.domain, object: first.object };
|
||||
const merge = mergeDeclarations(group);
|
||||
if (merge.merged === undefined || merge.conflicts.length > 0) {
|
||||
results.push({ merged: merge.merged, notices: merge.notices, conflicts: merge.conflicts, applied: false });
|
||||
continue;
|
||||
}
|
||||
const current = await deps.store.describe(ref);
|
||||
const plan = planSchemaChanges(merge.merged, current);
|
||||
const conflicts = [...merge.conflicts, ...plan.conflicts];
|
||||
if (conflicts.length > 0 || !planIsNonDestructive(plan)) {
|
||||
results.push({ merged: merge.merged, notices: [...merge.notices, ...plan.notices], conflicts, applied: false });
|
||||
continue;
|
||||
}
|
||||
if (plan.changes.length > 0) await deps.store.applyChanges(ref, plan.changes);
|
||||
results.push({
|
||||
merged: merge.merged,
|
||||
notices: [...merge.notices, ...plan.notices],
|
||||
conflicts: [],
|
||||
applied: true,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
async describe(address) {
|
||||
return deps.store.describe(address);
|
||||
},
|
||||
|
||||
async listObjects() {
|
||||
return deps.store.listObjects();
|
||||
},
|
||||
|
||||
/** ① 系统写:世界已发生的事实,免门,即时抛事件 */
|
||||
async systemWrite(write: SystemWrite) {
|
||||
await requireSchema(write.address);
|
||||
await deps.store.upsert(write.address, write.key, write.values);
|
||||
await deps.events.publish(
|
||||
dataEvent(
|
||||
write.address,
|
||||
write.key,
|
||||
Object.keys(write.values),
|
||||
'update',
|
||||
write.taskId,
|
||||
{ kind: 'ledger-write', ref: `${write.cause.kind}:${write.cause.ref}` },
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
/** ② 草稿写:读己之写,正式版不可见,**不产生数据事件** */
|
||||
async draftWrite(write: DraftWrite) {
|
||||
const schema = await requireSchema(write.address);
|
||||
const existing = await deps.store.selectByKeys(write.address, [write.key]);
|
||||
const baseline: Record<string, unknown> = {};
|
||||
const row = existing[0];
|
||||
if (row !== undefined) {
|
||||
for (const field of Object.keys(write.patch)) baseline[field] = row[field];
|
||||
}
|
||||
await deps.drafts.put(write.taskId, write.address, {
|
||||
key: pickKey({ ...write.key }, schema.primaryKey.length > 0 ? schema.primaryKey : Object.keys(write.key)),
|
||||
patch: write.patch,
|
||||
tombstone: write.tombstone ?? false,
|
||||
baseline,
|
||||
});
|
||||
},
|
||||
|
||||
/** ③ 放行写:冲突检查 → 原子合并 → 此刻抛事件 */
|
||||
async releaseWrite(taskId): Promise<ReleaseWriteResult> {
|
||||
const conflicts = await this.checkConflicts(taskId);
|
||||
if (conflicts.length > 0) throw new DraftConflictError(conflicts);
|
||||
|
||||
const drafts = await deps.drafts.list(taskId);
|
||||
if (drafts.length === 0) return { mergedRows: 0, emittedEvents: 0 };
|
||||
|
||||
const pending: EnvelopeDraft[] = [];
|
||||
|
||||
await deps.store.transaction(async (store, draftStore) => {
|
||||
for (const draft of drafts) {
|
||||
const ref: ObjectRef = { domain: draft.domain, object: draft.object };
|
||||
if (draft.tombstone) {
|
||||
await store.deleteRow(ref, draft.key);
|
||||
pending.push(
|
||||
dataEvent(ref, draft.key, [], 'delete', taskId, { kind: 'ledger-write', ref: `release:${taskId}` }),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const existing = await store.selectByKeys(ref, [draft.key]);
|
||||
await store.upsert(ref, draft.key, draft.patch);
|
||||
pending.push(
|
||||
dataEvent(
|
||||
ref,
|
||||
draft.key,
|
||||
Object.keys(draft.patch),
|
||||
existing.length === 0 ? 'insert' : 'update',
|
||||
taskId,
|
||||
{ kind: 'ledger-write', ref: `release:${taskId}` },
|
||||
),
|
||||
);
|
||||
}
|
||||
await draftStore.clear(taskId);
|
||||
});
|
||||
|
||||
for (const envelope of pending) await deps.events.publish(envelope);
|
||||
return { mergedRows: drafts.length, emittedEvents: pending.length };
|
||||
},
|
||||
|
||||
async query(query: LedgerQuery): Promise<LedgerPage> {
|
||||
const { rows, total } = await deps.store.select(query);
|
||||
return { rows, total };
|
||||
},
|
||||
|
||||
async taskView(taskId, query): Promise<LedgerPage> {
|
||||
const ref: ObjectRef = { domain: query.address.domain, object: query.address.object };
|
||||
const schema = await requireSchema(ref);
|
||||
const [official, drafts] = await Promise.all([
|
||||
deps.store.select(query),
|
||||
deps.drafts.listForObject(taskId, ref),
|
||||
]);
|
||||
const rows = overlay(official.rows, drafts, schema.primaryKey);
|
||||
return { rows, total: rows.length };
|
||||
},
|
||||
|
||||
async checkConflicts(taskId) {
|
||||
const drafts = await deps.drafts.list(taskId);
|
||||
if (drafts.length === 0) return [];
|
||||
const byObject = new Map<string, Array<(typeof drafts)[number]>>();
|
||||
for (const d of drafts) {
|
||||
const k = `${d.domain}.${d.object}`;
|
||||
const bucket = byObject.get(k);
|
||||
if (bucket === undefined) byObject.set(k, [d]);
|
||||
else bucket.push(d);
|
||||
}
|
||||
const findings = [];
|
||||
for (const [, group] of byObject) {
|
||||
const first = group[0]!;
|
||||
const ref: ObjectRef = { domain: first.domain, object: first.object };
|
||||
const schema = await requireSchema(ref);
|
||||
const rows = await deps.store.selectByKeys(
|
||||
ref,
|
||||
group.map((g) => g.key),
|
||||
);
|
||||
const officialByKey = new Map(rows.map((r) => [keyOf(pickKey(r, schema.primaryKey)), r]));
|
||||
findings.push(...detectConflicts(group, officialByKey));
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
async discardDrafts(taskId) {
|
||||
await deps.drafts.clear(taskId);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user