zizi b0bc7a8745 框架: 技能按动作-对象重组 + 先审后入创作闭环
一、技能重组(动作-对象命名)
- 旧目录 clean/confirm/continuation/db/detect/embed/… 重组为
  clean-book-text/decide-candidate/write-next-chapter/access-database/
  check-content-consistency/embed-knowledge/…(git 识别为 rename,内容保持)
- agents/*.md、AGENTS.md/CLAUDE.md 收编、example_skill 登记表同步新名

二、先审后入创作闭环(本次核心)
正文接受从"机械门一过就写正典"改为"机械门+语义审查双通过+用户批准+单事务原子提交",
DB 级兜底,编排层跳步即被硬拒。
- candidate_cas.py + example_candidate_cas(109):持久化 CAS 状态链
- fact_delta.py + example_fact_delta/example_fact_ledger(106):结构化事实增量,
  模型只提六型闭集增量+正文证据引文,仅用户批准的增量随正文同事务入账本
- projection_registry.py + example_projection_run(107):投影登记与恢复
- acceptance_state.py:接受前置实时状态重读
- lesson_registry.py + example_lesson(108):经验升格链,禁止自动升格
- DDL 105:example_candidate 增 semantic_status/semantic_report_sha256
- write_canonical.accept:语义兜底+同事务合并增量+登记投影;
  run_writer_pipeline/persist_writer_run/run_writer_semantic_detector/step2 接入全链
- claude_runtime:兼容新 CLI modelUsage 信息字段

三、审查修复(独立子代理四维审查后)
- 事实增量 propose→approve 翻态正道,不撞唯一键
- 冻结配置探针重刷(CLI 2.1.211→2.1.231 漂移),profileSha256/adapterVersion 再登记
- 可视化合同悬空路径/五六空间矛盾、 SoT 旧技能名漂移、行尾空白清理

测试:离线 65 套 + 真实库集成 5 套(CAS/接受故障注入/事实增量/投影/经验升格)+ 回放 79 项全绿。
创作内容(docs/design、生成正文 artifacts)按"框架与创作分开"未入本提交。
2026-08-14 10:24:08 +08:00

66 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""升格作品命令共用的 PostgreSQL 会话级 advisory lock。
锁连接是独立 autocommit 会话,只执行加锁和解锁 SQL,不参与任何业务事务。调用方可让该
context manager 跨越 LLM/嵌入调用持续存活;退出受保护区时,无论成功或异常都会显式解锁并
关闭会话。backup 的 restore/rehearse 可直接导入 ``upgrade_work_lock`` 复用同一互斥边界。
"""
import hashlib
import struct
from contextlib import contextmanager
import psycopg
LOCK_NAMESPACE = b"muse:upgrade-work-lock:v1"
class UpgradeWorkLockUnavailable(RuntimeError):
"""同租户、同作品已有另一条升格命令持锁。"""
def __init__(self, tenant_id, work_id):
self.tenant_id = tenant_id
self.work_id = work_id
super().__init__(
f"作品 tenant={tenant_id} work={work_id} 已有升格命令正在处理;"
"本次未执行任何业务操作"
)
def advisory_lock_keys(tenant_id, work_id):
"""把租户与作品稳定映射为 PostgreSQL 接受的两个有符号 int32 锁键。"""
identity = f"{int(tenant_id)}:{int(work_id)}".encode("ascii")
digest = hashlib.sha256(LOCK_NAMESPACE + b"\0" + identity).digest()
return struct.unpack(">ii", digest[:8])
@contextmanager
def upgrade_work_lock(dsn, tenant_id, work_id, *, connect=None):
"""非阻塞获取同书会话锁,失败立即抛错,finally 中释放并关闭专用连接。
``connect`` 仅供离线测试注入;生产默认使用 ``psycopg.connect``。返回的两个锁键可用于
调试记录,调用方不得使用该专用连接执行任何业务 SQL。
"""
connector = connect or psycopg.connect
keys = advisory_lock_keys(tenant_id, work_id)
connection = connector(dsn, autocommit=True)
acquired = False
try:
row = connection.execute(
"SELECT pg_try_advisory_lock(%s, %s)", keys
).fetchone()
acquired = bool(row and row[0] is True)
if not acquired:
raise UpgradeWorkLockUnavailable(tenant_id, work_id)
yield keys
finally:
try:
if acquired:
connection.execute("SELECT pg_advisory_unlock(%s, %s)", keys)
finally:
# 即使显式解锁 SQL 失败,关闭会话也会由 PostgreSQL 释放会话级锁。
connection.close()