diff --git a/CHANGELOG.md b/CHANGELOG.md index 6275b97..60eb9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- Make Keel's validated `app-schema` the canonical in-memory input to Swift, + C++, and Kotlin client generation. Rivet now keeps only a fail-closed + Keel-to-RVT1 type adapter instead of reconstructing a parallel codegen schema + model, while preserving generated source and schema snapshot compatibility. +- Synchronize the English and Chinese CI guide with the pinned Ubuntu 24.04 + runner and remove a stale comment about the now-opaque State descriptor. + ## 0.7.0 - Stage Racket's bundled foreign runtime libraries (`libeay32.dll`, diff --git a/README.md b/README.md index 7ef1413..df5234a 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,7 @@ The maintained Taskboard also has platform-native [UI end-to-end gates](docs/nat - [x] Kotlin typed-client generation - [x] clean-runner build/package/verify evidence for every supported desktop architecture - [x] **Phase 7 — desktop production graduation** — real Linux install/upgrade evidence, first-party update installation adapters, native UI end-to-end gates, and characterization-protected maintainability work; see the [graduation record](docs/production-roadmap.md) -- [ ] **Phase 8 — 1.0 contract stabilization** — incrementally source generated-client inputs from Keel, evaluate Float64/map/structured-error RVT1 extensions only with cross-runtime golden and fuzz evidence, and align portable distribution metadata without moving platform installer policy into Keel +- [ ] **Phase 8 — 1.0 contract stabilization** — use Keel as the canonical generated-client schema, evaluate Float64/map/structured-error RVT1 extensions only with cross-runtime golden and fuzz evidence, and align portable distribution metadata without moving platform installer policy into Keel - [ ] **Phase 9 — first-class device application delivery** — after the Windows/macOS/Linux 1.0 line is stable, add generated iOS/iPadOS/tvOS/visionOS/watchOS and Android projects, explicit embedded-runtime or companion roles, signing, packaging, and real-device verification ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 7d9afde..e06ada4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -343,7 +343,7 @@ CI 会验证 Racket、C++、Swift 与 Kotlin 协议实现,在三个桌面平 - [x] Kotlin 类型客户端生成 - [x] 覆盖每个受支持桌面架构的干净 runner 构建/打包/验证证据 - [x] **Phase 7 —— 桌面生产晋级** —— 真实 Linux 安装/升级证据、一方更新安装适配器、原生 UI 端到端门禁,以及由特征测试保护的可维护性改造;详见[晋级记录](docs/production-roadmap.md) -- [ ] **Phase 8 —— 1.0 契约稳定化** —— 渐进地让生成客户端以 Keel 为规范输入;Float64、map、结构化 error 等 RVT1 扩展只有在跨 runtime golden 与 fuzz 证据齐备时才引入;对齐可移植分发元数据,但平台安装器策略不进入 Keel +- [ ] **Phase 8 —— 1.0 契约稳定化** —— 生成客户端统一以 Keel 为规范 Schema;Float64、map、结构化 error 等 RVT1 扩展只有在跨 runtime golden 与 fuzz 证据齐备时才引入;对齐可移植分发元数据,但平台安装器策略不进入 Keel - [ ] **Phase 9 —— 一等设备应用交付** —— Windows/macOS/Linux 1.0 主线稳定之后,再提供生成式 iOS/iPadOS/tvOS/visionOS/watchOS 与 Android 项目,明确 embedded-runtime 或 companion 角色,并补齐签名、打包和真机验证 ## 许可证 diff --git a/docs/architecture.md b/docs/architecture.md index a4a7b8f..9f1d23f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -145,11 +145,12 @@ forms, Event/State operations, `serve`/`serve-fds`, diagnostics, and schema snapshots. The registry structs, RPC procedures, mutable State cells, locks, and request custodians are runtime-owned implementation details. -Build tools load the application in a fresh namespace and call -`backend-schema`. That one immutable, data-only snapshot is the supported -reflection boundary; generators do not reach into runtime structs. This keeps -code generation deterministic and lets the server's concurrency machinery -evolve without turning internal representation into an accidental API. +Build tools load the application in a fresh namespace, project its immutable +`backend-schema` snapshot into a Keel `app-schema`, and use that canonical +contract as the input to code generation. Generators do not reach into runtime +structs or maintain a second schema object model. This keeps code generation +deterministic and lets the server's concurrency machinery evolve without +turning internal representation into an accidental API. The terminal-ownership state machine for active requests lives in the private `backend-pending` module. It owns admission capacity, exactly-once terminal @@ -179,12 +180,13 @@ Typed generated clients are layered on top of this codec. The current schema sup `rivet/keel` projects the immutable `backend-schema` snapshot into Keel's transport-neutral `app-schema`. Keel owns the common type, schema, lifecycle, -and canonical introspection vocabulary shared with Glaze; Rivet continues to -own RVT1 framing, typed native code generation, embedded-runtime lifecycle, -and the WinUI, SwiftUI/AppKit, and GTK4/Wayland adapters. The projection does -not enable Keel's additional value tags on RVT1. RVT1 v1 Errors also remain -bounded diagnostic strings, so projected commands advertise no structured -error codes until that wire boundary changes deliberately. +canonical introspection vocabulary, and the in-memory objects consumed by +Rivet's native code generators. Rivet continues to own RVT1 framing, the +Keel-to-RVT1 type adapter, generated-language policy, embedded-runtime +lifecycle, and the WinUI, SwiftUI/AppKit, and GTK4/Wayland adapters. The +projection does not enable Keel's additional value tags on RVT1. RVT1 v1 +Errors also remain bounded diagnostic strings, so projected commands advertise +no structured error codes until that wire boundary changes deliberately. Migrating a Swift call site from the pre-namespace emission is mechanical — qualify the type once: diff --git a/docs/ci.md b/docs/ci.md index 7069f78..cec8edc 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -11,7 +11,7 @@ uses: - Windows: `windows-latest` for the WinUI 3 host; - macOS: both `macos-latest` and `macos-15`, so current Swift and the pinned Xcode 16 release environment both compile the SwiftUI/AppKit host; -- Linux: `ubuntu-latest` with GTK4 development files and an embeddable Racket +- Linux: `ubuntu-24.04` with GTK4 development files and an embeddable Racket CS runtime. `raco rivet build` is the required host-compilation gate. Add `package` and @@ -73,7 +73,7 @@ jobs: - run: raco rivet build linux-host: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 - name: Set up embeddable Racket CS diff --git a/docs/ci.zh-CN.md b/docs/ci.zh-CN.md index f14ef81..05bc3d4 100644 --- a/docs/ci.zh-CN.md +++ b/docs/ci.zh-CN.md @@ -10,7 +10,7 @@ CI 应使用与发布任务相同类别的 runner 和工具链: - Windows:用 `windows-latest` 编译 WinUI 3 宿主; - macOS:同时使用 `macos-latest` 与 `macos-15`,分别覆盖当前 Swift 和 固定的 Xcode 16 发布环境; -- Linux:用 `ubuntu-latest`,安装 GTK4 开发文件,并准备可嵌入的 Racket +- Linux:用 `ubuntu-24.04`,安装 GTK4 开发文件,并准备可嵌入的 Racket CS runtime。 `raco rivet build` 是必需的宿主编译门禁。若还要验证分发布局,可继续执行 @@ -69,7 +69,7 @@ jobs: - run: raco rivet build linux-host: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 - name: Set up embeddable Racket CS diff --git a/rivet-cli/codegen.rkt b/rivet-cli/codegen.rkt index c363969..6fa3b0e 100644 --- a/rivet-cli/codegen.rkt +++ b/rivet-cli/codegen.rkt @@ -22,7 +22,8 @@ (define (generate-clients! project) (define backend (project-path project (project-ref project 'backend))) - (define-values (rpcs events states records enums) (load-schema backend)) + (define-values (rpcs events states records enums) + (load-schema backend #:id (project-identifier project))) (define module-name (project-ref project 'module)) (define entry-name (project-ref project 'entry)) (define display-name (project-display-name project)) diff --git a/rivet-cli/codegen/model.rkt b/rivet-cli/codegen/model.rkt index b3c1673..7271b4d 100644 --- a/rivet-cli/codegen/model.rkt +++ b/rivet-cli/codegen/model.rkt @@ -1,51 +1,131 @@ #lang racket/base (require racket/list + (prefix-in keel: keel) "../project.rkt") -(provide (struct-out schema-rpc) - (struct-out schema-event) - (struct-out schema-state) - (struct-out schema-record) - (struct-out schema-enum) +(provide schema-rpc? + schema-rpc-name + schema-rpc-arg-names + schema-rpc-arg-types + schema-rpc-result-type + schema-event? + schema-event-name + schema-event-type + schema-state? + schema-state-name + schema-state-type + schema-record? + schema-record-name + schema-record-field-names + schema-record-field-types + schema-enum? + schema-enum-name + schema-enum-cases load-schema schema-name resolve-device-rpcs upper-first record-native-name) -(struct schema-rpc (name arg-names arg-types result-type) #:transparent) -(struct schema-event (name type) #:transparent) -(struct schema-state (name type) #:transparent) -(struct schema-record (name field-names field-types) #:transparent) -(struct schema-enum (name cases) #:transparent) +(define schema-rpc? keel:schema-command?) +(define schema-event? keel:schema-event?) +(define schema-state? keel:schema-state?) +(define schema-record? keel:schema-record?) +(define schema-enum? keel:schema-enum?) +(define-namespace-anchor model-namespace-anchor) -(define (load-schema backend) +;; Native generators still target RVT1's deliberately smaller type system. +;; Keep that transport mapping in one adapter while Keel remains the canonical +;; in-memory schema. Unsupported Keel types fail here instead of being silently +;; approximated by a generator. +(define (keel-type->rvt1-type type) + (cond + [(keel:scalar-type? type) + (case (keel:scalar-type-name type) + [(Bool Int64 String Bytes) (keel:scalar-type-name type)] + [(Unit) 'Void] + [(Value) 'Any] + [else + (raise-arguments-error 'load-schema + "Keel type is not supported by RVT1" + "type" type)])] + [(keel:list-type? type) + (list 'List (keel-type->rvt1-type (keel:list-type-element type)))] + [(keel:optional-type? type) + (list 'Optional + (keel-type->rvt1-type (keel:optional-type-element type)))] + [(keel:reference-type? type) + (string->symbol (keel:reference-type-name type))] + [else + (raise-arguments-error 'load-schema + "Keel type is not supported by RVT1" + "type" type)])) + +(define (schema-rpc-name rpc) + (string->symbol (keel:schema-command-name rpc))) + +(define (schema-rpc-arg-names rpc) + (for/list ([parameter (in-list (keel:schema-command-parameters rpc))]) + (string->symbol (keel:schema-parameter-name parameter)))) + +(define (schema-rpc-arg-types rpc) + (for/list ([parameter (in-list (keel:schema-command-parameters rpc))]) + (keel-type->rvt1-type (keel:schema-parameter-type parameter)))) + +(define (schema-rpc-result-type rpc) + (keel-type->rvt1-type (keel:schema-command-result rpc))) + +(define (schema-event-name event) + (string->symbol (keel:schema-event-name event))) + +(define (schema-event-type event) + (keel-type->rvt1-type (keel:schema-event-type event))) + +(define (schema-state-name state) + (string->symbol (keel:schema-state-name state))) + +(define (schema-state-type state) + (keel-type->rvt1-type (keel:schema-state-type state))) + +(define (schema-record-name record) + (string->symbol (keel:schema-record-name record))) + +(define (schema-record-field-names record) + (for/list ([field (in-list (keel:schema-record-fields record))]) + (string->symbol (keel:schema-field-name field)))) + +(define (schema-record-field-types record) + (for/list ([field (in-list (keel:schema-record-fields record))]) + (keel-type->rvt1-type (keel:schema-field-type field)))) + +(define (schema-enum-name enum) + (string->symbol (keel:schema-enum-name enum))) + +(define (schema-enum-cases enum) + (map string->symbol (keel:schema-enum-cases enum))) + +(define (load-schema backend #:id [id "rivet.codegen"]) ;; Backends register declarations as a module side effect. A fresh ;; namespace keeps separate projects from sharing a registry accidentally. + ;; Attach Keel from the caller namespace so the isolated Rivet instance + ;; returns canonical structs recognized by these accessors; only Rivet's + ;; mutable backend registry must be instantiated per project. (define ns (make-base-namespace)) + (namespace-attach-module + (namespace-anchor->namespace model-namespace-anchor) + 'keel + ns) (parameterize ([current-namespace ns]) (dynamic-require backend #f) - (define get-schema (dynamic-require 'rivet/backend 'backend-schema)) - (define schema (get-schema)) - (values - (for/list ([info (in-list (hash-ref schema 'rpcs))]) - (define arguments (hash-ref info 'arguments)) - (schema-rpc (hash-ref info 'name) - (map (lambda (argument) (hash-ref argument 'name)) arguments) - (map (lambda (argument) (hash-ref argument 'type)) arguments) - (hash-ref info 'result))) - (for/list ([info (in-list (hash-ref schema 'events))]) - (schema-event (hash-ref info 'name) (hash-ref info 'type))) - (for/list ([info (in-list (hash-ref schema 'states))]) - (schema-state (hash-ref info 'name) (hash-ref info 'type))) - (for/list ([info (in-list (hash-ref schema 'records))]) - (define fields (hash-ref info 'fields)) - (schema-record (hash-ref info 'name) - (map (lambda (field) (hash-ref field 'name)) fields) - (map (lambda (field) (hash-ref field 'type)) fields))) - (for/list ([info (in-list (hash-ref schema 'enums))]) - (schema-enum (hash-ref info 'name) (hash-ref info 'cases)))))) + (define current-schema + (dynamic-require 'rivet/keel 'current-backend-app-schema)) + (define schema (current-schema #:id id)) + (values (keel:app-schema-commands schema) + (keel:app-schema-events schema) + (keel:app-schema-states schema) + (keel:app-schema-records schema) + (keel:app-schema-enums schema)))) (define (schema-name value) (if (symbol? value) (symbol->string value) value)) diff --git a/rivet-cli/codegen/snapshot.rkt b/rivet-cli/codegen/snapshot.rkt index b18c3ff..14b7e29 100644 --- a/rivet-cli/codegen/snapshot.rkt +++ b/rivet-cli/codegen/snapshot.rkt @@ -33,7 +33,8 @@ (define (schema-snapshot project) (define backend (project-path project (project-ref project 'backend))) - (define-values (rpcs events states records enums) (load-schema backend)) + (define-values (rpcs events states records enums) + (load-schema backend #:id (project-identifier project))) (define device-rpcs (resolve-device-rpcs project rpcs 'schema-snapshot)) (parameterize ([current-records records] [current-enums enums]) diff --git a/rivet/backend.rkt b/rivet/backend.rkt index 6e8868a..b550619 100644 --- a/rivet/backend.rkt +++ b/rivet/backend.rkt @@ -73,10 +73,10 @@ ;; Keeping it private lets state-set! preserve the same behavior outside serve. (define current-state-commit-guard (make-parameter (lambda (thunk) (thunk)))) -;; `state-info` is public through struct-out, so keep update-order bookkeeping -;; private instead of adding a field that would break constructor/pattern source -;; compatibility. Weak identity keys also avoid retaining externally constructed -;; State values after the application releases them. +;; Keep update-order bookkeeping outside the opaque State descriptor. Weak +;; identity keys avoid retaining State values after the application releases +;; them and let the descriptor representation evolve without widening the +;; public API. (define state-update-locks (make-weak-hasheq)) (define state-update-locks-lock (make-semaphore 1)) diff --git a/tests/documentation.rkt b/tests/documentation.rkt index 9dd71d2..5f3b69a 100644 --- a/tests/documentation.rkt +++ b/tests/documentation.rkt @@ -124,7 +124,7 @@ (for ([expected (in-list '("windows-latest" "macos-latest" "macos-15" - "ubuntu-latest" + "ubuntu-24.04" "setup-embed-racket" "raco rivet build"))]) (check-file-contains ci-guide expected)) diff --git a/tests/keel.rkt b/tests/keel.rkt index 0aec6fa..d0f5568 100644 --- a/tests/keel.rkt +++ b/tests/keel.rkt @@ -1,8 +1,13 @@ #lang racket/base (require rackunit + racket/runtime-path rivet - (prefix-in keel: keel)) + (prefix-in keel: keel) + (prefix-in codegen: "../rivet-cli/codegen/model.rkt")) + +(define-runtime-path schema-matrix-backend + "fixtures/schema-matrix-backend.rkt") (define-enum AccessLevel (viewer editor owner)) @@ -40,6 +45,18 @@ (check-equal? encoded (keel:encode-app-schema schema) "Keel introspection encoding is deterministic") (check-equal? (keel:decode-app-value encoded) (keel:app-schema->app-value schema)) +;; Code generation isolates each application's mutable Rivet registry in a +;; fresh namespace, but it must still return canonical Keel schema values from +;; the one shared Keel module instance. +(define-values (codegen-commands codegen-events codegen-states + codegen-records codegen-enums) + (codegen:load-schema schema-matrix-backend #:id "rivet.codegen-test")) +(check-true (andmap keel:schema-command? codegen-commands)) +(check-true (andmap keel:schema-event? codegen-events)) +(check-true (andmap keel:schema-state? codegen-states)) +(check-true (andmap keel:schema-record? codegen-records)) +(check-true (andmap keel:schema-enum? codegen-enums)) + (check-exn #rx"Unit is valid only as a command result" (lambda () (backend-schema->app-schema (hash 'rpcs