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
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ jobs:
- uses: actions/setup-node@v5
with:
node-version: "22"
- name: Yjs export hash del corpus compartido
# Emite el hash de Yjs de ambos corpus (ascii + unicode) y self-checkea contra golden.json:
# caza drift de Yjs (bump con impacto de encoding). La aserción BLOQUEANTE de paridad yrs↔Yjs
# vive en Weft.Determinism.Tests (job `test`, per-PR); este job es informativo (FU-012/CHARTER-09).
- name: Yjs export hash del corpus compartido (ascii + unicode, self-check golden)
working-directory: tests/determinism-yjs
run: |
npm install
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
id: AILOG-2026-07-15-001
title: "CHARTER-09: client-id determinista en el FFI de yrs + gate de paridad cross-impl (determinism-yjs) per-PR"
status: accepted
created: 2026-07-15
agent: claude-opus-4-8
confidence: high
review_required: true
reviewed_by: Jose Villaseñor Montfort
reviewed_at: 2026-07-15
review_outcome: approved
risk_level: medium
eu_ai_act_risk: not_applicable
nist_genai_risks: []
iso_42001_clause: []
observability_scope: none
tags: [ffi-boundary, abi-bump, determinism, yrs, yjs, cross-impl-parity, client-id, utf16, gate]
related: [AIDEC-2026-07-15-001, AILOG-2026-07-13-003]
originating_charter: CHARTER-09-client-id-determinista-en-el-ffi-de-yrs-gate-de
---

# AILOG: CHARTER-09 — client-id determinista + gate de paridad cross-impl (determinism-yjs)

## Summary

Despacho de CHARTER-09 (FU-012): expone la siembra de `client_id` determinista en el FFI de yrs y **promueve el
gate de determinismo cross-implementación (`determinism-yjs`, T058) de informativo a aserción per-PR bloqueante**.
El riesgo central **R1 se cumple**: yrs produce exports **byte-idénticos** a Yjs sobre el corpus compartido
(ASCII y unicode) → el determinismo de Weft es **por formato** (encoding v1 de Yjs/yrs), no un accidente de esta
versión de yrs (constitución **P-III**). Alcance **yrs-only** decidido; la promoción cross-engine (Loro) se
difiere a **FU-016**. Sin auditoría externa (no cierra hito).

## Actions Performed

1. **FFI yrs — siembra de client_id (ABI v1→v2)**: `weft_doc_new_with_client_id(u64, out)` en
`native/weft-yrs-ffi/src/lib.rs` (`Options { client_id: ClientID::new(id), offset_kind: Utf16 }`), con
**guard `client_id < 2^53`** → `WEFT_ERR_OUT_OF_BOUNDS` (yrs 0.26+ codifica los client IDs en 53 bits;
`ClientID::new` tiene `debug_assert!(value & MASK == 0)` y corrompería en release sin el guard). `WEFT_ABI_VERSION`
1→2 + declaración en `include/weft_ffi.h`. Símbolo verificado exportado (`nm -D`).
2. **Binding .NET**: `weft_doc_new_with_client_id` en `NativeMethods.cs`; `YrsDoc.Create(ulong)`;
**`YrsEngine.CreateDoc(ulong)` método CONCRETO** (no en `ICrdtEngine` — ver AIDEC decisión 1);
`ExpectedAbiVersion` 1→2 en `NativeLibraryResolver.cs`.
3. **Golden + corpus unicode**: `apply.mjs` parametrizado (corpus por argumento + self-check contra
`golden.json`); `corpus-unicode.json` (BMP acentuado + CJK + astrales/emoji → índices UTF-16, R6);
`golden.json` comprometido con los hashes de Yjs de ambos corpus.
4. **Aserción de paridad per-PR (BLOQUEANTE)**: `Yrs_export_matches_yjs_golden` (Theory ascii+unicode) en
`Weft.Determinism.Tests` — aplica el corpus con yrs vía `CreateDoc(clientId)`, converge, y asierta
`sha256(ExportState) == golden`. Corre en el job `test` existente (per-PR, costo ~0). **2/2 verde.**
5. **Job Node informativo promovido**: `release.yml determinism-yjs` corre ambos corpus (`npm test`) y
**self-checkea** su hash de Yjs contra `golden.json` (caza drift de Yjs); permanece `continue-on-error`.
README del harness actualizado (estado → aserción per-PR yrs).
6. **Backlog**: FU-012 → `closed`; **FU-016** registrado (promoción cross-engine Loro vía `set_peer_id`).

## Modified Files

**Nativo**: `native/weft-yrs-ffi/src/lib.rs` (fn + guard + ABI v2), `native/weft-yrs-ffi/include/weft_ffi.h`.
**Binding**: `src/Weft.Core/Yrs/NativeMethods.cs`, `YrsDoc.cs`, `YrsEngine.cs`, `NativeLibraryResolver.cs`.
**Gate**: `tests/Weft.Determinism.Tests/DeterminismTests.cs` (test de paridad),
`tests/determinism-yjs/{apply.mjs, package.json, golden.json (new), corpus-unicode.json (new), README.md}`,
`.github/workflows/release.yml` (step del job). **Gobernanza**: `.straymark/follow-ups-backlog.md`
(FU-012 closed, FU-016), `.straymark/charters/09-*.md` (status), AIDEC-2026-07-15-001 (new).

## Risk

- **R1 (medio-alto, del Charter) — paridad yrs↔Yjs**: **RESUELTO POSITIVO.** El test asierta 2/2 (ascii+unicode):
yrs == Yjs byte-idéntico. El gate se fija bloqueante; no fue necesario el plan B (dejarlo informativo).
- **R2 (medio) — client_id ≥ 2^53**: mitigado con el guard en la frontera (`WEFT_ERR_OUT_OF_BOUNDS`); alinea con
el `debug_assert` de `ClientID::new`. El corpus usa 1/2/3 (seguros).
- **R3 (bajo) — ABI bump v1→v2**: bump atómico Rust (`WEFT_ABI_VERSION`) + .NET (`ExpectedAbiVersion`) en el mismo
PR; el export es aditivo (no cambia `weft_doc_new`); desalineación → error explícito de `NativeLibraryResolver`.
- **R5 (bajo, del Charter) — índices UTF-16 en unicode**: mitigado con evidencia — la variante unicode (surrogate
pairs astrales) pasa la aserción, confirmando la paridad de índices UTF-16 (`OffsetKind::Utf16`).

## Verification

```bash
# Shim yrs: compila + símbolo exportado + ABI v2
cd native/weft-yrs-ffi && cargo build --release && nm -D ../target/release/libweft_yrs_ffi.so | grep weft_doc_new_with_client_id

# Hash de Yjs de ambos corpus + self-check contra golden.json
cd ../../tests/determinism-yjs && npm install && npm test # ✓ ascii + unicode coinciden con golden

# Aserción de paridad per-PR (bloqueante) + suite completa
cd ../.. && dotnet test tests/Weft.Determinism.Tests -c Release # Yrs_export_matches_yjs_golden 2/2
dotnet test Weft.sln -c Release # suite completa intacta
```

## Follow-ups

Derivado del alcance yrs-only de CHARTER-09. No bloquea nada:

- **Follow-up (cross-engine, baja)**: promover la siembra de client-id de capacidad **concreta de `YrsEngine`** a
capacidad **cross-engine** — `CreateDoc(clientId)` en `ICrdtEngine` (o una interfaz opcional tipo
`INativeVersioning`) + `weft_loro_doc_new_with_peer_id` en `weft-loro-ffi` (Loro vía `set_peer_id`), para
habilitar un gate de determinismo Loro↔referencia si/cuando se quiera. **Trigger**: when se requiera paridad
determinista para el motor Loro. **Destination**: mini-charter. **Cost**: M.

## Additional Notes

- El test de paridad localiza `tests/determinism-yjs/` subiendo desde `AppContext.BaseDirectory` hasta la raíz del
repo (no requiere copiar el corpus al output del test); funciona local y en CI (checkout completo).
- La convergencia del test .NET espeja `apply.mjs` **exactamente** (delete sin guard de longitud, `syncPasses`
del corpus, hash del `ExportState` de la réplica 0) — cualquier divergencia de esquema rompería la paridad.

## Approval

Trabajo de frontera nativa (`risk_level: medium`, `review_required: true`) con ABI bump. El operador decidió
ex-ante el alcance (yrs-only, aserción per-PR) y autorizó ejecución continua. Verificación local citada; el CI del
PR valida la aserción per-PR bloqueante en toda la matriz. Compañero de AIDEC-2026-07-15-001.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
id: AIDEC-2026-07-15-001
title: "CHARTER-09: placement de la siembra de client-id (YrsEngine concreto vs ICrdtEngine) y forma del golden de paridad"
status: accepted
created: 2026-07-15
agent: claude-opus-4-8
confidence: high
review_required: true
reviewed_by: Jose Villaseñor Montfort
reviewed_at: 2026-07-15
review_outcome: approved
risk_level: medium
eu_ai_act_risk: not_applicable
nist_genai_risks: []
iso_42001_clause: []
tags: [ffi-boundary, determinism, yrs, yjs, cross-impl-parity, abi, engine-abstraction, client-id]
related: [AILOG-2026-07-15-001, weft-speckit-estado]
originating_charter: CHARTER-09-client-id-determinista-en-el-ffi-de-yrs-gate-de
---

# AIDEC: placement de la siembra de client-id y forma del golden de paridad

> Registra las dos decisiones sustantivas de CHARTER-09 (FU-012), anticipadas en el Charter §Tasks
> como candidatas a AIDEC, más el resultado del riesgo central **R1** (paridad byte-idéntica yrs↔Yjs).

## Context

FU-012 promueve el gate de determinismo cross-implementación (`determinism-yjs`, T058) de informativo a
aserción. La paridad byte-idéntica con Yjs exige **client-ids deterministas**, que el binding de yrs no
exponía. El operador decidió **alcance yrs-only** (el gate es yrs↔Yjs; Loro es otro formato) y **aserción
per-PR barata** (test .NET en el job `test` existente). Dos formas quedaban abiertas: dónde vive la
capacidad de siembra, y cómo se estructura el golden.

---

## Decisión 1 — Placement de la siembra de client-id

### Problem

`weft_doc_new_with_client_id` (FFI) necesita una superficie .NET. ¿Va en la interfaz compartida
`ICrdtEngine` (P-IV: abstracción de motor viva) o como capacidad concreta de `YrsEngine`?

### Alternatives Considered

- **A1 — `CreateDoc(ulong clientId)` en `ICrdtEngine`, `LoroEngine` lanza `NotSupported`.** Mantiene la
interfaz simétrica, pero introduce una capacidad que un motor **no honra en runtime** — tensiona P-IV
(la abstracción "promete" algo que un impl rechaza) y contradice el patrón ya establecido en el repo
(`INativeVersioning?` como capacidad **opcional** vía propiedad, no un método que lanza). **Rechazada.**
- **A2 — capacidad opcional tipo `INativeVersioning`** (interfaz `ISeedableClientId` expuesta por propiedad).
P-IV-correcta y descubrible, pero sobre-ingeniería para un solo método cuyo único consumidor es el test
de paridad (yrs-específico por naturaleza: el gate es yrs↔Yjs). **Rechazada por ahora.**
- **A3 (elegida) — `CreateDoc(ulong clientId)` como método CONCRETO de `YrsEngine`.** No toca `ICrdtEngine`
(que conserva `CreateDoc()` sin parámetro). El test de paridad usa `YrsEngine` directo. Honesto: la
capacidad es yrs-específica, no se declara en la abstracción algo que Loro no da hoy.

### Rationale

El gate de paridad es **intrínsecamente yrs↔Yjs** (misma familia de formato v1). Poner la siembra en la
interfaz compartida obligaría a Loro a implementarla (o a lanzar), sin beneficio para el gate y tensando
P-IV. La capacidad concreta en `YrsEngine` es el mínimo honesto; la promoción a capacidad **cross-engine**
(Loro vía `set_peer_id`) se difiere a **FU-016** — a materializar si/cuando se quiera un gate de determinismo
para Loro. El ABI bump (**v1→v2**, `weft_doc_new_with_client_id` aditivo) y el guard `client_id < 2^53`
(encoding de 53 bits de yrs 0.26+; `ClientID::new` tiene `debug_assert` de ello y corrompería en release)
son mecánicos y quedan documentados en el AILOG.

### Consequences

- `ICrdtEngine` intacto; `Weft.Versioning`/broker/relay no cambian. Superficie nueva mínima.
- **FU-016** registrado (promoción cross-engine Loro). No bloquea nada.

---

## Decisión 2 — Forma del golden y dónde asierta (bloqueante) la paridad

### Problem

¿Cómo se estructura el hash golden y qué componente **asierta** la paridad de forma bloqueante, respetando
el presupuesto de minutos de CI?

### Alternatives Considered

- **B1 — job Node en `release.yml` asertivo (quitar `continue-on-error`), pasar `WEFT_GOLDEN_HASH`.** Fiel al
diseño original del harness, pero la paridad **no se verifica per-PR** (release es `workflow_dispatch`) y
añade un job Node bloqueante. **Rechazada.**
- **B2 (elegida) — golden de Yjs comprometido (`golden.json`) + aserción per-PR en `Weft.Determinism.Tests`.**
`apply.mjs` produce el hash de Yjs de cada corpus → se compromete en `golden.json` (`ascii`/`unicode`). El
**test .NET** (`Yrs_export_matches_yjs_golden`, Theory ascii+unicode) aplica el corpus con yrs y asierta
`sha256(export) == golden` — corre en el job `test` existente (**bloqueante de facto, per-PR, costo ~0**).
El **job Node** de `release.yml` queda `continue-on-error` pero **self-checkea** su hash de Yjs contra el
mismo `golden.json` → caza **drift de Yjs** (bump con impacto de encoding).

### Rationale

Un único golden comprometido sirve a **ambos** lados: el test .NET verifica yrs↔golden (la paridad que
importa) donde es barato (job existente); el job Node verifica Yjs↔golden (vigencia del golden) donde el
entorno Node ya existe (release). Separa las dos preguntas —"¿yrs iguala a Yjs?" (bloqueante) y "¿el golden
sigue siendo el hash real de Yjs?" (informativo)— sin duplicar costo de CI.

### Consequences

- Regenerar `golden.json` es un paso **deliberado** documentado (README) para cambios de corpus, distinguible
de un drift accidental (que el self-check del job Node destapa).
- El corpus unicode (BMP acentuado + CJK + astrales) ejercita los índices **UTF-16** (R6) en el mismo gate.

---

## Resultado de R1 (riesgo central del Charter)

**La paridad byte-idéntica yrs↔Yjs SE CUMPLE.** El test `Yrs_export_matches_yjs_golden` pasa **2/2** (ascii +
unicode): yrs produce exactamente los hashes de Yjs (`27a8...3243` ascii, `afd1...9e02` unicode). Por tanto el
gate se fija como **bloqueante** (no fue necesario el plan B de "dejarlo informativo"). Bonus: la variante
unicode (con surrogate pairs astrales) confirma también la **paridad de índices UTF-16** (mitiga R5 del Charter
con evidencia, no argumentación). El determinismo de Weft queda demostrado **por formato**, no por versión de yrs.

## Approval

**Approved**: 2026-07-15 por `Jose Villaseñor Montfort`, en revisión interactiva. El operador decidió ex-ante el
alcance yrs-only (Loro diferido) y la aserción per-PR (AskUserQuestion al declarar CHARTER-09), y autorizó la
ejecución continua. Compañero de AILOG-2026-07-15-001.
Loading
Loading