import type { AdoptionLegAction, AdoptionNotMigrated } from "./wire.js"; /** * 身份值的编码。 * · `verbatim` —— 列里存的就是 principal 本身; * · `memory-scope` —— core 的 v2 typed scope 键(`user:` / `proj:/…`); * · `rule-owner-key` —— 规则桶键 `sha256("principal:" + principal)` 的十六进制([ref] 车二的 * `buildRuleOwnerKey`)。身份在**hash 原像**里,所以「旧值 → 新值」这一对必须**算**出来, * 不能拿 principal 本身去比 —— 这正是 [ref] 里残留计数恒报 0 的根因:旧式按明文 principal * 查 `owner_key`,那个谓词在这张表上**永远匹配不到任何一行**。 */ export type IdentityEncoding = "verbatim" | "memory-scope" | "rule-owner-key"; /** * 腿的执行形。 * · `bulk-rebind` —— 通用的等值 UPDATE(绝大多数腿); * · `session-policy-rekey` —— 逐行**重算主键**(policy_key = sha256([sessionId, principal]),身份在键的 * hash 原像里,等值 UPDATE 够不着); * · `blob-rewrite` —— 逐行改写 **JSON 载荷列**里的身份字段; * · `rule-owner-rekey` —— 规则桶的**整桶改键**(owner_key = sha256 原像里带身份,与 session-policy-rekey * 同族)。与那条腿的差别:桶键**不含**任何逐行变量(只有 principal),所以不必逐行扫出来重算 —— * 一对 (fromKey, toKey) 算一次,一条等值 UPDATE 走完全表。 * * 🔴 `blob-rewrite` 为什么必须存在(codex 交叉复审 R1-F2,亲核属实):本仓有三张表的 JSON 载荷是**真源**, * 身份列只是它的投影,而读面读的是载荷: * · `workflow_run.run` —— `get()` 是 `JSON.parse(run)` 且**只**覆盖 `rev`(store 行内注写死), * 所以只改 `scope` 列的话 `get().scope` 仍是旧身份;调用方把它回传给 scope-guarded 的 `update()` * 就再也命中不了已迁移的行 = **活的 workflow 被挂死**; * · `background_agent.record_json` —— store 自己的注写着「真源在 record_json,摘要投影截断是诚实形」; * · `checkpoint.checkpoint` —— core `Checkpoint.scope` 的类型注写着「Multi-tenant isolation key」。 * 只迁投影 = 库里两份身份说法不一致,而**说了算的那份没迁**。这一形在空表上完全无声(集成种子若把载荷 * 写成 `{}` 就永远测不出来)。 */ export type LegKind = "bulk-rebind" | "session-policy-rekey" | "blob-rewrite" | "rule-owner-rekey"; /** `blob-rewrite` 腿的坐标。 */ export interface BlobRewriteSpec { /** JSON 载荷列(真源)。 */ readonly column: string; /** 定位行的主键列(逐行改写要按它回写)。 */ readonly pk: readonly string[]; /** 载荷里携带身份值的**顶层**字段(值等于旧身份时才改 —— 绝不盲改)。 */ readonly fields: readonly string[]; /** 除 `matchColumn` 外还要 OR 上的匹配列(同表另一根轴;漏了它,只在那根轴上带旧身份的行找不到)。 */ readonly alsoMatch?: readonly string[]; /** * 该表自己的乐观并发守卫列(三张载荷表都有 `rev`)。 * * 🔴 为什么必须绑它(codex R2-F1,亲核属实):载荷改写是「读-改-写」,中间那道窗里若有一次正常业务写 * 提交,盲写会把它**整条盖掉** —— 而这三张表的正常写路径本来都是 rev-CAS 守着的,收编凭什么可以跳过? * 绑上之后:`affected === 0` ⇒ 有人在窗里动过这一行 ⇒ **响亮抛**,整个事务回滚(两侧字节零变更), * 而不是静默丢一次别人的写。 */ readonly revColumn: string; /** * 载荷里**内嵌**的 rev 字段名(有的表把 rev 同时写进 JSON 真源)。在场则与列上的 `rev + 1` 同步推进 * —— 否则改写之后列与载荷各说一个版本号,而这两处正是那些店的 CAS 读写两端。 * 缺席 = 该载荷没有版本字段(core `Checkpoint` 就没有;它的 rev 纯粹是服务端列)。 */ readonly revInPayload?: string; } export interface RebindLegSpec { /** 腿的稳定标识(回执 `legs[].store`)= `<表名>#<身份轴>`。下游按它对账,改名 = wire 破坏。 */ readonly leg: string; readonly table: string; readonly kind: LegKind; readonly action: AdoptionLegAction; /** 携带同一个身份值、必须一起改的列(键列的字节孪生 `*_key` 与可读列并列时两个都在)。 */ readonly columns: readonly string[]; /** WHERE 谓词匹配的那一列(必属 {@link columns})。 */ readonly matchColumn: string; readonly encoding: IdentityEncoding; /** * 唯一键**去掉身份轴**之后剩下的列。`undefined` = 没有任何唯一键含这根轴 ⇒ 重写不可能撞键 * (preflight 对它是**结构性无冲突**,不是「没查」)。`[]` = 唯一键**就是**这根轴本身 * (agent_memory_engine_cursor 的 PK(scope))⇒ 两侧各有任意一行即冲突。 */ readonly residualKey: readonly string[] | undefined; /** 仅 `blob-rewrite` 腿在场。 */ readonly blob?: BlobRewriteSpec; readonly why: string; } /** * 全部重绑腿。**次序即执行序**(同一事务内顺序跑):同表两轴相邻,workflow 家族相邻 * (183 §4.3「run + journal 同事务」在本车是「**全部腿同一个事务**」的更强形)。 */ export declare const REBIND_LEGS: readonly RebindLegSpec[]; /** * server 侧**按设计不迁**的面(183 §12 裁决号在案)。这份清单进回执的 `notMigratedByDesign`, * 让「按设计不迁」与「漏了没迁」在黑盒上可分(183 §10.5)。 * * 🔴 D1 三员为什么同族:它们全是**按对齐窗口分桶的治理瞬态计数**(usage_window / cost_quota / * rate_limit),窗口语义与身份绑在一起搬过去要跨窗口对齐,复杂度与价值不成比例;清零重计的代价是 * 新身份在当前窗口拿到一份新额度 —— 一次性部署级动作里这是可接受的,而且**方向偏松不偏紧**, * 不会把人锁在门外。 * 🔴 D2 三员为什么不改:它们是**谁在什么时候做了什么**的史实字段。改写史实 = 自造审计。 */ /** * 🔴 本数组的成员集合必须与 {@link NOT_MIGRATED_FACES}(冻结词表)**全等** —— 词表说有而这里没有 * 就是一条下游按名写了断言、回执里却永远不出现的幽灵成员。等式由 `test/adoption-arc.test.ts` 钉住。 * (建表时真发生过:`outcome-ledger` 一度只进了词表 —— 它压根没有身份轴,属于「无可迁」而不是 * 「按设计不迁」,两者混在一起会让 D2 的语义从「史实不改写」滑成「凡是没迁的都算设计」。) */ export declare const NOT_MIGRATED_BY_DESIGN: readonly AdoptionNotMigrated[]; /** * 受影响部署配置清单(183 §6 表的 server 投影)。`migrated` **恒 false** —— 这些配置住在别的部署单元, * 收编动作够不到;183 §3.3 的明令是「改不到的配置,不假装改了」。清单在场就是「无缺席」那一半, * 「不迁必红」那一半由消费端围栏(§10.4)执行。 */ export interface ConfigTemplate { readonly deployment: string; readonly key: string; /** `%TO%` 在铸清单时替换成 `toPrincipal`。 */ readonly requiredValue: string; } export declare const AFFECTED_CONFIG_TEMPLATES: readonly ConfigTemplate[]; /** * 目的地身份值的**列宽约束**(codex 交叉复审 R1-F3,亲核属实)。 * * 🔴 病:请求面收 ≤190 字符的 principal,而迁移腿要把它写进**最窄**只有 `VARCHAR(64)` 的列 * (`sandbox_image_index.tenant_id`),两根键列还是 `VARBINARY(190)`(**字节**,不是字符),memory scope * 更是在 principal 外面再套前缀 + 百分号编码(非 ASCII 膨胀约 9 倍)。于是一个**合法**请求可以跑到腿的 * 中途才被库以截断/超宽打回 —— 那时 phase 停在 INTENT,每次重发与每次 boot 扫描都会再撞一次同样的墙。 * * 🔴 姿势:在**铸行之前**按最窄真列校验并 typed 拒(fail-closed)。**不**在这里加自动截断——截断身份 * 就是把 A 的数据交给 B。 * * 另一条可选解是把 `tenant_id` 拓宽到与其它身份列同宽(183/codex 的「统一身份契约」建议)。本车不动 * 别的特性的 schema:那是一次跨车道的列变更,该由镜像面属主拍。此处如实登记这条不对称。 */ export declare const IDENTITY_WIDTH_LIMITS: readonly { readonly what: string; readonly unit: "chars" | "bytes"; readonly max: number; }[]; /** memory scope 列宽(两方言 VARCHAR(190) 同宽;段编码后才是真长度)。 */ export declare const MEMORY_SCOPE_MAX_CHARS = 190; /** * 收编身份的**键空间卫生门**([ref],验真后修)。纯函数、零 I/O ⇒ 在铸任何行之前判。 * * ── 病 ────────────────────────────────────────────────────────────────────────────────────────── * `adoption_log.from_principal` 上的 UNIQUE 是 183 D7「同一个源不许被收编两次」的**数据库强形**, * 而这张表的 MySQL 臂是 `VARCHAR(190) … COLLATE utf8mb4_bin`。`utf8mb4_bin` 虽然是 binary collation, * **却仍然是 PAD SPACE 的**(MySQL 只有 `utf8mb4_0900_*` 族是 NO PAD)—— 等值比较与唯一键检查会把两侧 * 右填充到等长。于是 `"user:a"` 与 `"user:a "` 在 MySQL 腿上是**同一个键**,在 PG 腿(`COLLATE "C"`, * 无填充)上是**两个键**。同一份代码、同一个请求,两个后端上结局不同: * · MySQL:第二次收编被 `adoption.source_already_bound` 拒 —— 一个运维完全看不懂的 409; * · PG:落第二行,弧照跑,却**一行都迁不到**(别的表里没有任何列存着带尾空格的那个身份)。 * * ── 为什么修在边界而不是把列改成 VARBINARY(本仓已有的字节孪生形)───────────────────────────── * 本仓对**同一族**问题已经裁过一次,逐字在 `plugins/approval-ask-store-sql.ts` 的 * `assertIdempotencyKeyShape` 头注里:「在边界上 fail-loud 拒掉,方言分歧面整个消失(比把列改 * VARBINARY 更窄、且对三形同时成立)」。这里的判据比那次还硬一层 —— **带首尾空白的 principal 在本仓 * 根本不可能是一个真身份**:`security.ts` 的 `principalFrom` 读头时逐字 `.trim()`,所以没有任何 * owner/scope 列里存得下带尾空格的值。收编一个这样的源身份,能迁到的行恒为零。 * 拒 = 把一个注定空转(或注定撞出误导性 409)的请求在**入库之前**变成一句说得清的 400。 * 改列则要动 schema + 基线 + 双库,却只把 MySQL 腿对齐到 PG 腿,对「这个值本来就不是合法身份」这件事 * 一个字都没说 —— 那才是治标。 */ export declare function checkAdoptionPrincipalShape(fromPrincipal: string, toPrincipal: string): string | undefined; /** * 目的地身份能不能被每一根被写列容纳?返回**人可读的拒绝理由**,`undefined` = 可以。 * 纯函数(零 I/O),所以校验发生在任何库动作之前。 */ export declare function checkAdoptionWidths(fromPrincipal: string, toPrincipal: string): string | undefined; /** `proj:` / `userproj:` 键的 tenant 段前缀(值对由 DISTINCT 扫描按前缀解出)。 */ export declare const MEMORY_PROJECT_SCOPE_PREFIXES: readonly string[]; /** * 一根身份轴上,「旧值 → 新值」的**值对**。`verbatim` 轴恒只有一对;`memory-scope` 轴的对数 = * 该 principal 名下 user 盘 1 个 + 每个 project 盘各 1 个(由库里 DISTINCT 扫出来,不靠猜)。 */ export interface IdentityValuePair { readonly from: string; readonly to: string; } /** `verbatim` 轴的值对(纯数据,零 I/O)。 */ export declare function buildVerbatimPair(fromPrincipal: string, toPrincipal: string): IdentityValuePair; /** * memory scope 键的重写(纯函数,给扫描出来的每个 scope 值算新值)。 * · `user:` ⇒ `user:`(逐字等值); * · `proj:/…` / `userproj:/…` ⇒ 只换 tenant 段,项目段原样; * · 其它(`org:` 组织盘、无前缀的旧不透明键)⇒ `undefined` = **不属这个 principal,不动**。 * * 🔴 为什么不用 `LIKE '%'`:段是**百分号编码**的(`user%3Aweb-demo`),`%` 在 LIKE 里是通配符, * 不逐字符转义就会误命中一大片。用「DISTINCT 扫 + 纯函数判前缀」把这道坑整个绕开:判定在 JS 里做, * 落到 SQL 的永远只有**等值**谓词。 */ export declare function rewriteMemoryScope(scope: string, fromPrincipal: string, toPrincipal: string): string | undefined; //# sourceMappingURL=plan.d.ts.map