Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Status: Draft, under maintainer review
- Initially proposed by: NoKV Lab
- Widened by: LoopX maintainers
- Date: 2026-08-05; revised 2026-09-07
- Date: 2026-08-05; revised 2026-09-12
- Scope: one provider-neutral LoopX authority contract with built-in file,
optional NoKV, and optional PostgreSQL provider profiles, complementing
[`host-integration-surface-v0`](../../reference/protocols/host-integration-surface-v0.md)
Expand Down Expand Up @@ -2600,6 +2600,32 @@ The planner neither reads a provider nor grants a lease, CAS receipt, or write
permission. This checkpoint closes one rule owner, not the remaining mutation
inventory or local-store/promotion qualification.

### Cross-RFC semantic and presentation conformance checkpoint (2026-09-12)

The TypeScript migration and this provider RFC now share one explicit Todo
semantic boundary. Python production callers import `todos/todo_semantics.py`
directly; `todos/projection.py` is retained only as an import-compatible facade
for external integrations. This is an ownership cleanup, not a second kernel.
The typed TypeScript `projection_delivery` union also owns the distinction
between mutation intent (`pending`/`not_required`) and provider readback
(`delivered`/`current`); unknown states fail closed before acknowledgement.

Presentation is canonical at the projection layer, not in the domain record.
`source_section` and `index` are the v0 wire shape's display coordinates, while
native records derive the same display section from role/archive state and use
timestamp plus Todo identity as a deterministic fallback instead of a fake
persistent index. The normalized presentation metadata is therefore one
contract even when the wire shapes differ. The same rule is exercised by the
production-scale fixture and by File, SQLite, and NoKV conformance arms.
Provider revision tokens remain provider-owned and are compared only for the
provider-specific replay rules; they are not normalized into Todo semantics.

This checkpoint changes read/ordering and compatibility-adapter semantics only:
it does not promote a provider, add a writer, alter the transaction decoder
delivered by #4280, or make Markdown a second authority. The shared RFC still
owns durable truth, recovery, cutover, and projection delivery; the TS RFC owns
business-rule ownership and caller deletion.

### Next delivery and parallel provider work

Markdown is a **permanent first-class readable projection**. Retire its database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- 状态:Draft,正在接受 maintainer review
- 最初提案方:NoKV Lab
- 扩展修订方:LoopX maintainer
- 日期:2026-08-05;修订于 2026-09-07
- 日期:2026-08-05;修订于 2026-09-12
- 范围:一个 provider-neutral 的 LoopX 权威合同,支持内置 file、可选 NoKV
与可选 PostgreSQL provider profile,用来补充
[`host-integration-surface-v0`](../../reference/protocols/host-integration-surface-v0.md)
Expand Down Expand Up @@ -2060,6 +2060,28 @@ unsupported-field fence。planner 不读取 provider,也不授予 lease、CAS
写权限。该检查点闭合的是一个规则 owner,不是剩余 mutation inventory 或本地
store/promotion 资格化。

### 跨 RFC 的语义与展示 conformance 检查点(2026-09-12)

TypeScript 重构 RFC 与本 provider RFC 现在共享一个显式的 Todo 语义边界。
Python 生产 caller 直接从 `todos/todo_semantics.py` 导入;`todos/projection.py`
只作为外部集成所需的 import 兼容 facade 保留,不再是第二个 kernel。这是 owner
收敛,不是新增一套规则。TypeScript 的 typed `projection_delivery` union 也明确区分
mutation intent(`pending`/`not_required`)与 provider readback(`delivered`/`current`);
未知状态在 acknowledgement 之前 fail closed。

展示语义属于 projection 层,而不是 domain record。`source_section` 与 `index` 是 v0
wire shape 的展示坐标;native record 根据 role/archive state 推导相同的展示 section,
并以时间戳和 Todo identity 做确定性回退,不制造假的持久 index。因此即使 wire shape
不同,normalized presentation metadata 仍只有一份 contract。同一规则由
production-scale fixture 以及 File、SQLite、NoKV conformance arm 共同覆盖。Provider
自己的 revision token 仍由各自 provider 管理,只用于 provider-specific replay 规则,
不被归一成 Todo 语义。

本检查点只改变 read/ordering 与兼容 adapter 语义:不晋升 provider、不增加 writer,
不改动 #4280 交付的 transaction decoder,也不把 Markdown 变成第二权威。共享 RFC
继续负责 durable truth、恢复、cutover 与 projection delivery;TS RFC 负责业务规则
owner 与 caller 删除。

### 下一步交付与并行 provider 工作

Markdown 是**长期保留的一等可读投影**。退役的是它的数据库及业务 writer 权威,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Status: Accepted, transaction-payoff phase in progress
- Proposed by: LoopX maintainers
- Date: 2026-08-15
- Last revised: 2026-09-10
- Last revised: 2026-09-12
- Scope: an incremental, replacement-first migration of the LoopX control-plane
core from Python to TypeScript without maintaining two semantic
implementations
Expand Down Expand Up @@ -192,11 +192,56 @@ this RFC follows the
It declares fixture impact, exercises every affected provider arm, and keeps
the read-only three-arm rehearsal as a separate promotion gate.

### Provider-neutral projection conformance checkpoint (2026-09-12)

The conformance boundary now has one projection-fixture builder for both the
legacy v0 and native Todo record shapes. It owns deterministic Unicode ordering,
read-model digest/field construction, and the compatibility-only conversion;
provider tests no longer hand-rebuild those fields. The scale envelope declares
status ordering explicitly and validates its counts, so changing JSON key order
cannot silently change which Todo receives a lease, successor, or archive role.

The File, SQLite, and NoKV suites now execute the same production-scale terminal
cases in both record shapes. A separate parity harness replays one seed,
observation, and lease sequence through all three isolated providers and compares
the logical head plus committed event/projection/receipt trace while ignoring
provider-specific revision tokens. This is conformance evidence, not a new
authority writer, provider default, or promotion claim; PostgreSQL remains under
its existing real-service qualification gate.

The old v0 consumer manifest remains readable and retains all existing fields.
Default Markdown capture still emits v0; this PR neither rewrites stored heads
nor auto-promotes a goal. The schema split is not permission to drop v0
provenance or change legacy ordering during a later migration.

### Canonical Todo presentation checkpoint (2026-09-12)

The authority boundary now treats presentation as a first-class projection
contract rather than naming it `legacy_projection`. A shared TS presentation
normalizer maps the v0 wire shape's `source_section`/`index` to
`display_section`/`display_order`, while native records derive their display
section from domain role/archive state and never receive a fake persisted
index. The normalized presentation contract is shared; the wire coordinate is
not a second Todo state machine.

Todo creation, terminal successor materialization, projection validation,
standing-decision ordering, and archive ordering all use the same presentation
owner. The canonical domain validator is shared by both wire shapes, and the
v0 record is produced by an adapter from a validated domain record. This
unifies the semantic owner without rewriting v0 heads or receipts.

Python read callers now import the semantic owner directly; the compatibility
facade is no longer an internal dependency. Python presentation sorting keeps
source `index` order when it is present and uses completion/update time plus
Todo identity for native records, so the compatibility shape cannot leak into
business eligibility or lifecycle decisions.

The next migration may persist an optional canonical `presentation` object, but
only after proving whether an imported section is provenance or current display
intent and after qualifying a stable display-order policy. Until then, native
display positions remain derived at the renderer boundary and must not affect
authority lifecycle decisions.

### Long-goal persistence is part of the migration payoff

The product target is at least ten elapsed days per goal, not a short-lived
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Status:Accepted,transaction-payoff 阶段进行中
- Proposed by:LoopX maintainers
- Date:2026-08-15
- Last revised:2026-09-10
- Last revised:2026-09-12
- Scope:LoopX 控制面核心从 Python 到 TypeScript 的增量、replacement-first
迁移;不长期维护两份语义实现
- Tracking issue:[#3225](https://github.com/huangruiteng/loopx/issues/3225)
Expand Down Expand Up @@ -151,10 +151,48 @@ replay、concurrency、归档压力与 hard-lease fence。该 fixture 是持久
声明 fixture 影响、覆盖所有受影响的 provider arm,并把只读三臂演练保留为独立的
promotion gate。

### Provider-neutral projection conformance 检查点(2026-09-12)

conformance 边界现在为 legacy v0 与 native Todo record 共用一个 projection-fixture
builder。它统一负责确定性的 Unicode 排序、read-model digest/field 构造,以及仅限
兼容层的转换;provider 测试不再手工重建这些字段。规模 envelope 显式声明 status
顺序并校验计数,因此 JSON key 顺序变化不会静默改变哪个 Todo 获得 lease、successor
或 archive 角色。

File、SQLite 与 NoKV suite 现在会在两种 record shape 上执行同一组生产规模 terminal
case。另有独立 parity harness,使用三个隔离 provider 重放同一条 seed、observation、
lease 序列,并在忽略 provider-specific revision token 后比较 logical head 以及已提交
的 event/projection/receipt trace。这是 conformance 证据,不是新的 authority writer、
provider 默认值或 promotion 声明;PostgreSQL 仍受现有真实服务资格化 gate 约束。

旧 v0 consumer manifest 继续可读,并保留所有已有字段。默认 Markdown capture 仍
输出 v0;本 PR 不改写已存 head,也不自动晋升 goal。schema 分层不等于允许后续迁移
丢失 v0 provenance 或改变旧排序。

### Canonical Todo 展示检查点(2026-09-12)

authority 边界现在把 presentation 作为一等 projection contract,而不再把它命名为
`legacy_projection`。共享的 TS presentation normalizer 会把 v0 wire shape 的
`source_section`/`index` 映射为 `display_section`/`display_order`;native record
则根据 domain 的 role/archive state 推导展示 section,绝不伪造持久化 index。两种
wire shape 共用同一份 normalized presentation contract,wire 坐标不构成第二套 Todo
state machine。

Todo creation、terminal successor materialization、projection validation、
standing-decision ordering 与 archive ordering 现在共用同一个 presentation owner。
两种 wire shape 共用 canonical domain validator,v0 record 只是从已校验 domain
record 经过 adapter 生成。这统一了语义 owner,但不重写 v0 head 或 receipt。

Python read caller 现在直接导入语义 owner;兼容 facade 不再是内部依赖。Python 的
展示排序在存在 source `index` 时保持其顺序,在 native record 上使用完成/更新时间
加 Todo identity 做确定性排序,因此兼容 shape 不会泄漏进业务 eligibility 或 lifecycle
decision。

后续迁移可以持久化可选的 canonical `presentation` object,但必须先证明导入的
section 到底是 provenance 还是当前 display intent,并资格化稳定的 display-order
策略。在此之前,native display position 仍在 renderer 边界派生,不能参与 authority
lifecycle decision。

### 长程持久化也是迁移收益的一部分

产品目标是单个 goal 至少持续十个自然日。shared-authority RFC 的
Expand Down
6 changes: 3 additions & 3 deletions loopx/control_plane/agents/agent_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
todo_item_is_actionable_open,
todo_item_is_deferred,
todo_item_task_class,
todo_projection_sort_key,
todo_presentation_sort_key,
)
from ..todos.summary_item import compact_todo_summary_item
from ..todos.user_gate import (
Expand Down Expand Up @@ -103,8 +103,8 @@ def _attach_agent_identity_contracts(

def _todo_task_class(item: dict[str, Any]) -> str:
return todo_item_task_class(item)
def _todo_projection_sort_key(item: dict[str, Any]) -> tuple[int, int]:
return todo_projection_sort_key(item)
def _todo_projection_sort_key(item: dict[str, Any]) -> tuple[int, int, str, str]:
return todo_presentation_sort_key(item)


def _todo_item_is_actionable_open(item: dict[str, Any]) -> bool:
Expand Down
44 changes: 30 additions & 14 deletions loopx/control_plane/coordination/coordination_projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,46 @@
requireAuthorityStoreId,
} from "./authority_store_codec.ts";
import {
canonicalCoordinationTodoRecord,
canonicalTodoDomainRecord,
TODO_DOMAIN_READ_RECORD_SCHEMA,
TODO_DOMAIN_RECORD_CONTRACT,
TODO_CANONICAL_READ_RECORD_FIELDS,
TODO_CANONICAL_READ_RECORD_SCHEMA,
} from "./coordination_state_contract.ts";
import {canonicalTodoRecord} from "./todo_presentation.ts";

export const COORDINATION_PROJECTION_MUTATION_EVENT_SCHEMA =
"loopx_coordination_projection_mutation_event_v0";
export const COORDINATION_PROJECTION_MUTATION_RECEIPT_SCHEMA =
"loopx_coordination_projection_mutation_receipt_v0";
export { TODO_CANONICAL_READ_RECORD_FIELDS, TODO_CANONICAL_READ_RECORD_SCHEMA };

/**
* Build the revision-bound Todo read model carried by a canonical projection.

Check warning on line 32 in loopx/control_plane/coordination/coordination_projection.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCVEr1hi2WmFx-_SpD4&open=AaCVEr1hi2WmFx-_SpD4&pullRequest=4283
*
* The read model is projection metadata, not another source of Todo meaning.

Check warning on line 34 in loopx/control_plane/coordination/coordination_projection.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCVEr1hi2WmFx-_SpD5&open=AaCVEr1hi2WmFx-_SpD5&pullRequest=4283
* Keeping its construction beside validation prevents shadow capture, native
* transactions, and conformance fixtures from drifting on schema fields or
* digest inputs. Callers still choose the legacy/native schema explicitly;
* this helper never performs a compatibility conversion.
*/
export function coordinationTodoReadModel(
records: readonly JsonObject[],
schemaVersion: unknown,
): JsonObject {
const isNative = schemaVersion === TODO_DOMAIN_READ_RECORD_SCHEMA;
if (!isNative && schemaVersion !== TODO_CANONICAL_READ_RECORD_SCHEMA) {
throw new AuthorityStoreProtocolError("coordination Todo read-model schema mismatch");
}
return {
schema_version: schemaVersion,
todo_count: records.length,
records_sha256: canonicalAuthoritySha256(records),
contract_fields: [...(isNative
? TODO_DOMAIN_RECORD_CONTRACT.fields
: TODO_CANONICAL_READ_RECORD_FIELDS)],
};
}

export interface CoordinationTodoProjectionIndex {
readonly todos: ReadonlyMap<string, JsonObject>;
readonly todo_ids: readonly string[];
Expand Down Expand Up @@ -155,22 +181,12 @@
)) {
throw new AuthorityStoreProtocolError("coordination Todo read-model field contract mismatch");
}
const validateRecord = domain ? canonicalTodoDomainRecord : canonicalCoordinationTodoRecord;
for (const [recordIndex, record] of records.entries()) {
validateRecord(record, `coordination Todo read record ${recordIndex}`);
canonicalTodoRecord(record, `coordination Todo read record ${recordIndex}`);
}
return readModel;
}

function todoReadModel(records: readonly JsonObject[], previous: JsonObject): JsonObject {
return {
schema_version: previous.schema_version,
contract_fields: previous.contract_fields,
todo_count: records.length,
records_sha256: canonicalAuthoritySha256(records),
};
}

function requireCompleteTodoReplacement(
previous: JsonObject | undefined,
replacement: JsonObject,
Expand Down Expand Up @@ -323,7 +339,7 @@
leases: sortedIds(leases.keys()).map((todoId) => leases.get(todoId)!),
...(readModel === undefined
? {}
: { todo_read_model: todoReadModel(nextTodos, readModel) }),
: { todo_read_model: coordinationTodoReadModel(nextTodos, readModel.schema_version) }),
}, "coordination projection");
if (value.todo_read_model !== undefined) {
validateCoordinationTodoReadModel(reduced, expectedGoalId);
Expand Down
16 changes: 5 additions & 11 deletions loopx/control_plane/coordination/local_authority_shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
} from "./authority_store.ts";
import { authorityUnicodeCompare, canonicalAuthorityBytes, canonicalAuthoritySha256 } from "./authority_store_codec.ts";
import {
TODO_CANONICAL_READ_RECORD_FIELDS,
coordinationTodoReadModel,
validateCoordinationTodoReadModel,
} from "./coordination_projection.ts";
import { FileAuthorityStore } from "./file_authority_store.ts";
Expand Down Expand Up @@ -688,15 +688,6 @@ function partitionsOf(head: JsonObject | null): JsonObject {
return partitions;
}

function todoReadModel(todos: readonly JsonObject[]): JsonObject {
return {
schema_version: "loopx_todo_canonical_read_record_v0",
todo_count: todos.length,
records_sha256: createHash("sha256").update(canonicalAuthorityBytes(todos)).digest("hex"),
contract_fields: [...TODO_CANONICAL_READ_RECORD_FIELDS],
};
}

/**
* Fold one partition into the candidate head. A v0 head (whole-snapshot
* observation) is accepted as the starting point with no partition markers.
Expand Down Expand Up @@ -732,7 +723,10 @@ export function composeLocalAuthorityShadowHead(
handoff_mode: handoffMode,
todos,
leases,
todo_read_model: todoReadModel(todos),
todo_read_model: coordinationTodoReadModel(
todos,
"loopx_todo_canonical_read_record_v0",
),
partitions,
...(base.capture_profile === undefined ? {} : {
capture_profile: base.capture_profile,
Expand Down
18 changes: 2 additions & 16 deletions loopx/control_plane/coordination/todo_archive_selection.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { JsonObject } from "../effect_program.ts";
import {isStandingDecisionReceipt} from "../todos/standing_decision.ts";
import {
authorityUnicodeCompare,
canonicalAuthorityObject,
requireAuthorityStoreId,
} from "./authority_store_codec.ts";
import {compareTodoPresentation} from "./todo_presentation.ts";

export const COORDINATION_TODO_ARCHIVE_SELECTION_SCHEMA =
"loopx_coordination_todo_archive_selection_v0";
Expand Down Expand Up @@ -45,21 +45,7 @@ function archiveLimit(value: unknown): number {
}

function archiveOrder(left: JsonObject, right: JsonObject): number {
const leftIndex = Number.isSafeInteger(left.index) && Number(left.index) >= 0
? Number(left.index) : null;
const rightIndex = Number.isSafeInteger(right.index) && Number(right.index) >= 0
? Number(right.index) : null;
if (leftIndex !== null || rightIndex !== null) {
if (leftIndex === null) return 1;
if (rightIndex === null) return -1;
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
}
const leftTime = typeof left.completed_at === "string"
? left.completed_at : typeof left.updated_at === "string" ? left.updated_at : "";
const rightTime = typeof right.completed_at === "string"
? right.completed_at : typeof right.updated_at === "string" ? right.updated_at : "";
if (leftTime !== rightTime) return authorityUnicodeCompare(leftTime, rightTime);
return authorityUnicodeCompare(String(left.todo_id), String(right.todo_id));
return compareTodoPresentation(left, right);
}

/** Select completed Todo ids without owning storage or applying mutations. */
Expand Down
Loading