Skip to content

feat(core): wire layer — typed envelopes + ts-rs single-source TypeScript (card e98bb804)#1

Merged
joelteply merged 3 commits into
mainfrom
feat/wire-layer-e98bb804
Jun 10, 2026
Merged

feat(core): wire layer — typed envelopes + ts-rs single-source TypeScript (card e98bb804)#1
joelteply merged 3 commits into
mainfrom
feat/wire-layer-e98bb804

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

The four traits are the in-process contract; this adds the wire
contract that crosses a transport (continuum's JTAG WebSocket being
consumer #1):

  • StateLayer: Ephemeral/Session/Persistent/Semantic cadence enum;
    defaults to Session so an unannotated update can never claim the
    60 Hz lane.
  • StateEnvelope (state-down): kind + revision + layer + opaque
    consumer payload. Positron frames; consumers fill.
  • CommandEnvelope (event-up): kind, consumer-defined command name,
    params, correlation_id, source.
  • CommandSource: Human | Observer{observer_id} — typed provenance on
    every action. An AI acting through a widget is first-class but
    never anonymous; audit/trust policies key off this without
    string-sniffing.
  • ObserverSpec: observer_id + budget_hz + explicit kind opt-ins
    (perception is budgeted, not ambient).

Single-source-of-truth flow: every wire type derives ts_rs::TS and
exports to npm/core/src/generated/ (TS_RS_EXPORT_DIR pinned in
.cargo/config.toml, regenerated by cargo test — drift cannot hide).
@positron/core npm skeleton re-exports the generated types; payload
interiors stay consumer-typed by design.

Tests: lossless round-trips, pinned CommandSource wire shape, layer
default. fmt + clippy clean.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

…ript (card e98bb804)

The four traits are the in-process contract; this adds the wire
contract that crosses a transport (continuum's JTAG WebSocket being
consumer #1):

- StateLayer: Ephemeral/Session/Persistent/Semantic cadence enum;
  defaults to Session so an unannotated update can never claim the
  60 Hz lane.
- StateEnvelope (state-down): kind + revision + layer + opaque
  consumer payload. Positron frames; consumers fill.
- CommandEnvelope (event-up): kind, consumer-defined command name,
  params, correlation_id, source.
- CommandSource: Human | Observer{observer_id} — typed provenance on
  every action. An AI acting through a widget is first-class but
  never anonymous; audit/trust policies key off this without
  string-sniffing.
- ObserverSpec: observer_id + budget_hz + explicit kind opt-ins
  (perception is budgeted, not ambient).

Single-source-of-truth flow: every wire type derives ts_rs::TS and
exports to npm/core/src/generated/ (TS_RS_EXPORT_DIR pinned in
.cargo/config.toml, regenerated by cargo test — drift cannot hide).
@positron/core npm skeleton re-exports the generated types; payload
interiors stay consumer-typed by design.

Tests: lossless round-trips, pinned CommandSource wire shape, layer
default. fmt + clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joelteply

Copy link
Copy Markdown
Contributor Author

VERDICT: BLOCK MERGE (adversarial review, commit 340f5d1)

Most of this PR is solid — but one generated type is a contract lie, in the layer whose whole job is contract truth.

Blocking finding

StateEnvelope.revision is revision?: bigint in TS, but the wire value is a JSON number. ts-rs maps u64bigint, which breaks both directions for a JSON consumer:

  • Consuming: JSON.parse yields a number; code like env.revision === 41n typechecks and silently never matches.
  • Producing: a TS host that satisfies the type with revision: 41n gets a runtime TypeError: Do not know how to serialize a BigInt from JSON.stringify.

So the obvious usage path on each side either silently misbehaves or throws. Fix: #[ts(type = "number")] on revision (documenting the 2^53 precision ceiling), or serialize revisions as strings. One-line change; then I'd approve.

Non-blocking risk notes

  • "Drift impossible" is overstated. cargo test regenerates the .ts files, but stale committed types only show up as a dirty git tree — and there is no CI (.github/ doesn't exist) running git status --short after tests. The mechanism works; nothing enforces it. (Verified clean for THIS commit — see evidence.)
  • ObserverSpec has no layers field, though StateEnvelope.layer docs say observers "may be subscribed to a subset of layers". The subscription shape can't express the layer filter it advertises.
  • @positron/core main/types point at raw .ts source; many toolchains won't compile TS out of node_modules. Fine for a skeleton, will need a build before first real consumer.
  • ts-rs emits warning: failed to parse serde attribute on skip_serializing_if — cosmetic (the #[ts(optional)] covers it; generated output is correct).

What checked out (verified, not vibes)

  • Drift check: fresh worktree at 340f5d1, ran cargo test -p positron-core (11 passed incl. 5 export tests), then git status --shortclean. Committed TS matches regeneration.
  • cargo clippy -p positron-core --tests -- -D warnings → clean. cargo fmt --check → clean.
  • Contract coherence: kind/revision semantics mirror the ViewState trait exactly (None = treat-as-new); StateLayer matches DESIGN.md's four layers; no widget vocabulary leaks into code (chat/continuum appear only as doc examples); commands stay consumer-owned (command: String, opaque params).
  • Safe defaults (no AttachRequest-flood analog): missing layersession (pinned by test, can't claim the 60 Hz lane); ObserverSpec.kinds empty = perceive nothing (opt-in); {"source":"observer"} without observer_id fails deserialization, and human carries no id to forge.
  • Wire shape pinned by tests: snake_case, internally-tagged CommandSource, round-trip + exact-JSON assertions.
  • npm/core/src/index.ts re-exports all five generated types.

🤖 Generated with Claude Code

…Spec.layers, CI drift gate (card e98bb804)

Review #1 (posted on the PR) blocked on a real bug: u64 revision
generated as TS bigint while the JSON wire carries a number —
JSON.parse yields number (comparison never matches), JSON.stringify
throws on bigint. Both consumer paths broken by the type. Now
#[ts(type = "number")] with the rationale documented; 2^53−1
monotonic revisions is not a real constraint.

Also from the review:
- ObserverSpec.layers: Vec<StateLayer> — the per-layer subscription
  the StateEnvelope docs advertised but the spec couldn't express.
  Same explicit-opt-in semantics as kinds (empty = none).
- .github/workflows/ci.yml: fmt, clippy -D warnings, test, then
  git diff --exit-code — the regeneration drift gate is now enforced,
  not just mechanically possible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joelteply

Copy link
Copy Markdown
Contributor Author

Re-review of b416add — APPROVE (block lifted)

All three blocking findings from the prior review are addressed; verified by inspection of the delta (340f5d1..b416add) and by running the full gate locally in a clean worktree at b416add.

  1. revision is TS numberwire.rs now carries #[ts(optional, type = "number")] on StateEnvelope.revision with an inline rationale (JSON wire is number; bigint breaks both JSON.parse and JSON.stringify; 2^53−1 monotonic revisions is not a real constraint). Generated StateEnvelope.ts has revision?: number.
  2. ObserverSpec.layerspub layers: Vec<StateLayer> added with empty=none semantics explicitly documented to match kinds; generated ObserverSpec.ts has layers: Array<StateLayer> with the same doc comment.
  3. CI drift gate — new .github/workflows/ci.yml runs cargo fmt --check, cargo clippy --workspace --tests -- -D warnings, cargo test --workspace (which regenerates the TS via TS_RS_EXPORT_DIR), then git diff --exit-code, in that order.

Local verification at b416add (temp worktree):

  • cargo test -p positron-core: 11/11 pass, including all five export_bindings_* tests
  • cargo fmt --check: clean
  • cargo clippy --workspace --tests -- -D warnings: exit 0
  • git status --short after tests: clean — committed generated TS matches regeneration

Non-blocking notes:

  • The drift gate uses git diff --exit-code, which won't catch a brand-new untracked generated file (e.g. a future #[ts(export)] struct whose .ts is never committed). Consider git diff --exit-code + git status --porcelain emptiness check, or git add -N . && git diff --exit-code.
  • ObserverSpec.layers has no #[serde(default)], so pre-b416add JSON without the field fails to deserialize. Acceptable pre-1.0 per the module's stated wire-stability policy, and consistent with kinds being required.
  • ts-rs emits a cosmetic "failed to parse serde attribute" note for skip_serializing_if during macro expansion; pre-existing, does not fail -D warnings.

🤖 Generated with Claude Code

…ip (card e98bb804)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joelteply joelteply merged commit 40b805c into main Jun 10, 2026
2 checks passed
@joelteply joelteply deleted the feat/wire-layer-e98bb804 branch June 10, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant