Archived
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,303 @@
|
||||
/**
|
||||
* 台账存储 adapter —— node-postgres 直写(编码规范 §1 / ADR-002)。
|
||||
*
|
||||
* 运行时 DDL/DML 由声明生成,ORM 静态 schema 不适用;`ledger` schema 的**唯一写者**就是本文件。
|
||||
* 标识符全部来自 @contracts 的 identifier 正则(小写字母开头、字母数字下划线),
|
||||
* 落库前再过一道 `quoteIdent` —— 拼接 SQL 的地方只有这里。
|
||||
*/
|
||||
import type { Pool, PoolClient } from 'pg';
|
||||
import type { LedgerKey, LedgerQuery, LedgerRow } from '@contracts';
|
||||
import { LEDGER_SCHEMA } from '@shared/db.ts';
|
||||
import type { DraftStore, LedgerStore, ObjectRef } from '../../port.ts';
|
||||
import type { MaterializedObject, MergedField } from '../domain/schema-merge.ts';
|
||||
import type { DraftEntry } from '../domain/task-view.ts';
|
||||
import { keyOf } from '../domain/task-view.ts';
|
||||
|
||||
const IDENT = /^[a-z][a-z0-9_]*$/;
|
||||
|
||||
function quoteIdent(name: string): string {
|
||||
if (!IDENT.test(name)) throw new Error(`非法标识符:${name}`);
|
||||
return `"${name}"`;
|
||||
}
|
||||
|
||||
/** 物理表名:<域>__<对象>;域与对象都过 IDENT,拼接安全 */
|
||||
export function tableNameOf(ref: ObjectRef): string {
|
||||
if (!IDENT.test(ref.domain) || !IDENT.test(ref.object)) {
|
||||
throw new Error(`非法台账地址:${ref.domain}.${ref.object}`);
|
||||
}
|
||||
return `${ref.domain}__${ref.object}`;
|
||||
}
|
||||
|
||||
const PG_TYPE: Readonly<Record<MergedField['type'], string>> = {
|
||||
text: 'text',
|
||||
number: 'double precision',
|
||||
integer: 'bigint',
|
||||
boolean: 'boolean',
|
||||
date: 'date',
|
||||
timestamp: 'timestamptz',
|
||||
json: 'jsonb',
|
||||
};
|
||||
|
||||
const FROM_PG: Readonly<Record<string, MergedField['type']>> = {
|
||||
text: 'text',
|
||||
'character varying': 'text',
|
||||
'double precision': 'number',
|
||||
numeric: 'number',
|
||||
bigint: 'integer',
|
||||
integer: 'integer',
|
||||
boolean: 'boolean',
|
||||
date: 'date',
|
||||
'timestamp with time zone': 'timestamp',
|
||||
'timestamp without time zone': 'timestamp',
|
||||
jsonb: 'json',
|
||||
json: 'json',
|
||||
};
|
||||
|
||||
type Queryable = Pick<Pool | PoolClient, 'query'>;
|
||||
|
||||
export function createLedgerStore(pool: Pool): LedgerStore {
|
||||
return buildStore(pool, pool);
|
||||
}
|
||||
|
||||
function buildStore(runner: Queryable, pool: Pool): LedgerStore {
|
||||
const store: LedgerStore = {
|
||||
async describe(ref) {
|
||||
const table = tableNameOf(ref);
|
||||
const cols = await runner.query<{ column_name: string; data_type: string }>(
|
||||
`select column_name, data_type from information_schema.columns
|
||||
where table_schema = $1 and table_name = $2 order by ordinal_position`,
|
||||
[LEDGER_SCHEMA, table],
|
||||
);
|
||||
if (cols.rows.length === 0) return undefined;
|
||||
|
||||
const pk = await runner.query<{ attname: string }>(
|
||||
`select a.attname from pg_index i
|
||||
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = any(i.indkey)
|
||||
where i.indrelid = $1::regclass and i.indisprimary`,
|
||||
[`${LEDGER_SCHEMA}.${quoteIdent(table)}`],
|
||||
);
|
||||
|
||||
const materialized: MaterializedObject = {
|
||||
domain: ref.domain,
|
||||
object: ref.object,
|
||||
columns: cols.rows.map((c) => ({ name: c.column_name, type: FROM_PG[c.data_type] ?? 'text' })),
|
||||
primaryKey: pk.rows.map((p) => p.attname),
|
||||
};
|
||||
return materialized;
|
||||
},
|
||||
|
||||
async listObjects() {
|
||||
const res = await runner.query<{ table_name: string }>(
|
||||
`select table_name from information_schema.tables
|
||||
where table_schema = $1 and table_name not like '\\_\\_%' order by table_name`,
|
||||
[LEDGER_SCHEMA],
|
||||
);
|
||||
return res.rows.flatMap((r) => {
|
||||
const idx = r.table_name.indexOf('__');
|
||||
if (idx <= 0) return [];
|
||||
return [{ domain: r.table_name.slice(0, idx), object: r.table_name.slice(idx + 2) }];
|
||||
});
|
||||
},
|
||||
|
||||
async applyChanges(ref, changes) {
|
||||
const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`;
|
||||
for (const change of changes) {
|
||||
switch (change.kind) {
|
||||
case 'create-table': {
|
||||
const cols = change.object.fields.map((f) => `${quoteIdent(f.name)} ${PG_TYPE[f.type]}`);
|
||||
const pk =
|
||||
change.object.primaryKey.length > 0
|
||||
? `, primary key (${change.object.primaryKey.map(quoteIdent).join(', ')})`
|
||||
: '';
|
||||
await runner.query(`create table if not exists ${table} (${cols.join(', ')}${pk})`);
|
||||
break;
|
||||
}
|
||||
case 'add-column':
|
||||
await runner.query(
|
||||
`alter table ${table} add column if not exists ${quoteIdent(change.field.name)} ${PG_TYPE[change.field.type]}`,
|
||||
);
|
||||
break;
|
||||
case 'widen-column':
|
||||
await runner.query(
|
||||
`alter table ${table} alter column ${quoteIdent(change.field.name)} type ${PG_TYPE[change.field.type]} ` +
|
||||
`using ${quoteIdent(change.field.name)}::${PG_TYPE[change.field.type]}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async select(query: LedgerQuery) {
|
||||
const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(query.address))}`;
|
||||
const params: unknown[] = [];
|
||||
const clauses = Object.entries(query.where).map(([field, value]) => {
|
||||
params.push(value);
|
||||
return `${quoteIdent(field)} = $${params.length}`;
|
||||
});
|
||||
const where = clauses.length > 0 ? ` where ${clauses.join(' and ')}` : '';
|
||||
const order =
|
||||
query.orderBy.length > 0
|
||||
? ` order by ${query.orderBy.map((o) => `${quoteIdent(o.field)} ${o.dir === 'desc' ? 'desc' : 'asc'}`).join(', ')}`
|
||||
: '';
|
||||
|
||||
const countRes = await runner.query<{ n: string }>(`select count(*)::text as n from ${table}${where}`, params);
|
||||
params.push(query.limit, query.offset);
|
||||
const rows = await runner.query<LedgerRow>(
|
||||
`select * from ${table}${where}${order} limit $${params.length - 1} offset $${params.length}`,
|
||||
params,
|
||||
);
|
||||
return { rows: rows.rows, total: Number(countRes.rows[0]?.n ?? '0') };
|
||||
},
|
||||
|
||||
async selectByKeys(ref, keys) {
|
||||
if (keys.length === 0) return [];
|
||||
const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`;
|
||||
const out: LedgerRow[] = [];
|
||||
for (const key of keys) {
|
||||
const params: unknown[] = [];
|
||||
const clauses = Object.entries(key).map(([field, value]) => {
|
||||
params.push(value);
|
||||
return `${quoteIdent(field)} = $${params.length}`;
|
||||
});
|
||||
if (clauses.length === 0) continue;
|
||||
const res = await runner.query<LedgerRow>(
|
||||
`select * from ${table} where ${clauses.join(' and ')} limit 1`,
|
||||
params,
|
||||
);
|
||||
out.push(...res.rows);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
async upsert(ref, key, values) {
|
||||
const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`;
|
||||
const merged: LedgerRow = { ...key, ...values };
|
||||
const fields = Object.keys(merged);
|
||||
if (fields.length === 0) return;
|
||||
const params = fields.map((f) => merged[f]);
|
||||
const placeholders = fields.map((_, i) => `$${i + 1}`);
|
||||
const keyFields = Object.keys(key);
|
||||
const updatable = fields.filter((f) => !keyFields.includes(f));
|
||||
const onConflict =
|
||||
keyFields.length === 0
|
||||
? ''
|
||||
: updatable.length === 0
|
||||
? ` on conflict (${keyFields.map(quoteIdent).join(', ')}) do nothing`
|
||||
: ` on conflict (${keyFields.map(quoteIdent).join(', ')}) do update set ` +
|
||||
updatable.map((f) => `${quoteIdent(f)} = excluded.${quoteIdent(f)}`).join(', ');
|
||||
await runner.query(
|
||||
`insert into ${table} (${fields.map(quoteIdent).join(', ')}) values (${placeholders.join(', ')})${onConflict}`,
|
||||
params,
|
||||
);
|
||||
},
|
||||
|
||||
async deleteRow(ref, key) {
|
||||
const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`;
|
||||
const params: unknown[] = [];
|
||||
const clauses = Object.entries(key).map(([field, value]) => {
|
||||
params.push(value);
|
||||
return `${quoteIdent(field)} = $${params.length}`;
|
||||
});
|
||||
if (clauses.length === 0) return;
|
||||
await runner.query(`delete from ${table} where ${clauses.join(' and ')}`, params);
|
||||
},
|
||||
|
||||
async transaction(fn) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('begin');
|
||||
const scoped = buildStore(client, pool);
|
||||
const result = await fn(scoped, createDraftStore(client));
|
||||
await client.query('commit');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('rollback');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
},
|
||||
};
|
||||
return store;
|
||||
}
|
||||
|
||||
/* ── 任务级草稿层:ledger.__draft ─────────────────────────────────────── */
|
||||
|
||||
export const DRAFT_TABLE = `${LEDGER_SCHEMA}.__draft`;
|
||||
|
||||
export const DRAFT_DDL = `
|
||||
create table if not exists ${DRAFT_TABLE} (
|
||||
task_id text not null,
|
||||
domain text not null,
|
||||
object text not null,
|
||||
key_hash text not null,
|
||||
key jsonb not null,
|
||||
patch jsonb not null,
|
||||
baseline jsonb not null,
|
||||
tombstone boolean not null default false,
|
||||
updated_at timestamptz not null default now(),
|
||||
primary key (task_id, domain, object, key_hash)
|
||||
);`;
|
||||
|
||||
type DraftRow = {
|
||||
task_id: string;
|
||||
domain: string;
|
||||
object: string;
|
||||
key: Record<string, string | number>;
|
||||
patch: LedgerRow;
|
||||
baseline: LedgerRow;
|
||||
tombstone: boolean;
|
||||
};
|
||||
|
||||
function toEntry(row: DraftRow): DraftEntry & ObjectRef {
|
||||
return {
|
||||
domain: row.domain,
|
||||
object: row.object,
|
||||
key: row.key,
|
||||
patch: row.patch,
|
||||
baseline: row.baseline,
|
||||
tombstone: row.tombstone,
|
||||
};
|
||||
}
|
||||
|
||||
export function createDraftStore(runner: Queryable): DraftStore {
|
||||
return {
|
||||
async list(taskId) {
|
||||
const res = await runner.query<DraftRow>(
|
||||
`select * from ${DRAFT_TABLE} where task_id = $1 order by updated_at`,
|
||||
[taskId],
|
||||
);
|
||||
return res.rows.map(toEntry);
|
||||
},
|
||||
async listForObject(taskId, ref) {
|
||||
const res = await runner.query<DraftRow>(
|
||||
`select * from ${DRAFT_TABLE} where task_id = $1 and domain = $2 and object = $3 order by updated_at`,
|
||||
[taskId, ref.domain, ref.object],
|
||||
);
|
||||
return res.rows.map(toEntry);
|
||||
},
|
||||
async put(taskId, ref, entry) {
|
||||
await runner.query(
|
||||
`insert into ${DRAFT_TABLE} (task_id, domain, object, key_hash, key, patch, baseline, tombstone)
|
||||
values ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
on conflict (task_id, domain, object, key_hash) do update
|
||||
set patch = ${DRAFT_TABLE}.patch || excluded.patch,
|
||||
tombstone = excluded.tombstone,
|
||||
updated_at = now()`,
|
||||
[
|
||||
taskId,
|
||||
ref.domain,
|
||||
ref.object,
|
||||
keyOf(entry.key as LedgerKey),
|
||||
JSON.stringify(entry.key),
|
||||
JSON.stringify(entry.patch),
|
||||
JSON.stringify(entry.baseline),
|
||||
entry.tombstone,
|
||||
],
|
||||
);
|
||||
},
|
||||
async clear(taskId) {
|
||||
await runner.query(`delete from ${DRAFT_TABLE} where task_id = $1`, [taskId]);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user