From 023f0167eb16a0cbf70ed65af395280476226a4d Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 5 Sep 2026 15:42:26 +0900 Subject: [PATCH 1/4] docs(sdk): brief semantic ConsoleWriter submission boundary --- ...its-through-the-existing-console-writer.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md diff --git a/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md b/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md new file mode 100644 index 000000000..8cb1e52b5 --- /dev/null +++ b/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md @@ -0,0 +1,135 @@ +# Submit semantic edits through the existing ConsoleWriter + +Status: root-approved implementation brief under the user’s Astra medium workflow. This closes the existing app #101 control-boundary mismatch; it adds no queue or playback behavior. Implementation starts only after the matching numbered spec is committed and synchronized. + +## Concrete problem and smallest product slice + +Inspected reviewed SDK #434 at `/private/tmp/miso-dx-sdk-observer`, HEAD +`8a19a84813230713e8f1604db04be4dccf653283`, specifically +`sdk/src/core/writer.ts`, its existing writer/type tests and ordinary SDK gates. +Inspected the authorized current app binding +`/private/tmp/miso-dx-app/src/lib/mixer/engine/console-writer.ts`. + +The SDK writer already stages `LaneEdit` objects and selects the next batch in +`#flushOnce`; it encodes only when invoking its encoded `submit(records,count)` +callback. The app consequently decodes those generated-ABI records into host +commands for another encoding step. The reviewed adapter console accepts +`submit(...edits: readonly LaneEdit[]): Promise`, and its SDK +console returns actual refusals as reports. One semantic callback on the SAME +writer removes the app's decode/re-encode bridge without replacing queue policy. + +## Proposed contract + +Support either encoded `submit(records,count)` or semantic +`submitEdits(edits: readonly LaneEdit[])`, each returning +`CommandReport | Promise`. Preserve `maximumBatch` and its current +default/validation. Reject both or neither at the constructor boundary, including +untyped JavaScript callers. No new queue, writer instance, scheduler, host +protocol, receipt type, Rust change, or runtime dependency. + +For strict source compatibility, preserve the currently exported `WriterOptions` +interface as the existing encoded contract. Add one exported semantic option type: + +```ts +export interface SemanticWriterOptions extends Omit { + readonly submit?: never; + readonly submitEdits: + (edits: readonly LaneEdit[]) => CommandReport | Promise; +} + +constructor(options: + | (WriterOptions & { readonly submitEdits?: never }) + | SemanticWriterOptions) +``` + +This keeps existing `WriterOptions["submit"]` non-optional and avoids breaking +consumers that extend the existing interface. Replacing `WriterOptions` itself +with a union would preserve ordinary constructor call sites but lose that source +compatibility; it is unnecessary for this slice. The root barrel already exports +all writer types, so the new type needs no hand-maintained export list. + +Normalize the selected callback once to an internal semantic submit function: +encoded mode calls its existing callback with `encodeLaneEdits(edits)` and +`edits.length`; semantic mode calls `submitEdits(edits)` directly. Keep both under +the one existing `#tail` and `#flushOnce`. Do not encode, decode, fabricate a +report, or introduce an extra queue in semantic mode. The selected edit array is +readonly at the public boundary; no new freezing/copying policy is necessary. + +All existing pending-map identity checks, insertion order, latest-wins staging, +coalescing stats, adaptive halve/grow behavior, escalation behavior, drain bounds, +and recovery of the flush chain remain unchanged. The callback receives the +selected addressed edits including kind names, optional values and smoothing; +it owns transport-specific encoding/validation. Successful accounting uses the +actual report's admitted count. Backpressure admits nothing and retains pending +edits; non-flow refusal still raises the existing MisoUsageError. Preserve the +existing FlushOutcome API (it is not a replacement CommandReport). + +App #101 can then use this binding, retaining its existing scheduling/report +association: + +```ts +new ConsoleWriter({ + submitEdits: async edits => { + const report = await console.submit(...edits); + this.#lastReport = report; + return report; + }, +}); +``` + +App #101 owns removing decodeRecords/generated ABI imports and the old host +acknowledgment bridge after integrating the reviewed SDK. This SDK issue does not +edit the app or block its ongoing public-host migration. No report fields or +appliedAtSample values are synthesized by this change. + +## Exact implementation paths + +- `sdk/src/core/writer.ts`: the single option type and constructor dispatch; + route the already-selected edits through that callback in #flushOnce. +- `sdk/test/writer-evals.mjs`: extend existing real-engine/async fixtures for + semantic dispatch rather than adding a second harness. +- `sdk/test/barrel-surface.ts`: constructor and public-type checks alongside + existing writer exports, including the new semantic options type. +- `sdk/README.md`: one concise example of the two mutually exclusive modes. +- New matching numbered `.github/ISSUE_SPECS/...` assigned and synchronized by + root before implementation; record decisions/checkpoints/evidence there. + +No generated declarations, ABI/catalog artifacts, package metadata, build +scripts, browser host, adapter, application, or Rust edits are required. + +## Minimum meaningful acceptance + +1. Keep every existing encoded writer eval passing. Reuse the real paused-engine + episode through `engine.console().submit(...edits)` to prove semantic async + admission, real backpressure, adaptive split and latest-wins final landing. + Existing helpers already provide lawful edits, queue fill/drain and reports. +2. Extend the existing deferred async race fixture in semantic mode: concurrent + flushes remain serialized; a newer same-address edit staged while the earlier + semantic batch is awaiting its actual report survives that report and is + submitted next. Observe addressed values/counts, outcomes and stats. +3. Exercise a real non-backpressure refusal in semantic mode and confirm the + existing escalation plus usable subsequent flush chain. No fake success or + weakened refusal assertions. A small constructor check rejects both/neither. +4. Typecheck both legacy annotated WriterOptions and SemanticWriterOptions, + reject mixed/neither constructor options and non-CommandReport callbacks, + prove the callback array and LaneEdit values readonly, and retain all existing + root-barrel type identities. No separate type harness. +5. Focused writer evals and types green -> pause for root's exact-path source + checkpoint. Then ordinary headless and package gates once; preserve existing + compiled Wasm artifacts and do not rebuild/repin on Darwin. + +Commands from the SDK repository root (existing artifact directory): + +```sh +bash scripts/check-sdk-types.sh +MISO_ENGINE_SDK_ARTIFACTS_HEX=$(node -e 'process.stdout.write(Buffer.from("/private/tmp/dx-393-current-artifacts").toString("hex"))') node --test sdk/test/writer-evals.mjs +bash scripts/check-sdk-headless.sh /private/tmp/dx-393-current-artifacts +bash scripts/sdk-package.sh check /private/tmp/dx-393-current-artifacts +``` + +The ordinary package gate includes generated-policy and fresh packed-consumer +checks. No new browser matrix, benchmark, harness, or allocator/protocol work. +A fresh independent Astra medium review follows the completed evidence, with +root handling checkpoint pushes and GitHub synchronization. + +Matching issue: misofm/engine#445. Root approval: preserve existing WriterOptions interface, add the separate semantic option and one callback dispatch over the same queue. From edfe7431b665a1c6350d5fb8eabee1479b2b1649 Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 5 Sep 2026 15:45:01 +0900 Subject: [PATCH 2/4] feat(sdk): submit semantic edits through the existing console writer --- sdk/src/core/writer.ts | 23 +++++++++++++--- sdk/test/barrel-surface.ts | 30 +++++++++++++++++++++ sdk/test/writer-evals.mjs | 54 +++++++++++++++++++++++++++----------- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/sdk/src/core/writer.ts b/sdk/src/core/writer.ts index d22c916ef..14c0d81f6 100644 --- a/sdk/src/core/writer.ts +++ b/sdk/src/core/writer.ts @@ -99,6 +99,12 @@ export interface WriterOptions { readonly maximumBatch?: number; } +/** Submit addressed edits directly through a semantic console transport. */ +export interface SemanticWriterOptions extends Omit { + readonly submit?: never; + readonly submitEdits: (edits: readonly LaneEdit[]) => CommandReport | Promise; +} + const BACKPRESSURE = ABI_LAYOUT.constants.commandReasons .find((row) => row.name === "backpressure")!.value; const RECORD_BYTES = ABI_LAYOUT.commandRecord.bytes; @@ -157,7 +163,7 @@ export function encodeLaneEdits(edits: readonly LaneEdit[]): Uint8Array { } export class ConsoleWriter { - readonly #submit: WriterOptions["submit"]; + readonly #submit: SemanticWriterOptions["submitEdits"]; readonly #maximumBatch: number; /** Insertion-ordered by key, which is what makes coalescing a map update rather than a scan. */ readonly #pending = new Map(); @@ -181,8 +187,17 @@ export class ConsoleWriter { #escalations = 0; #coalesced = 0; - constructor(options: WriterOptions) { - this.#submit = options.submit; + constructor(options: (WriterOptions & { readonly submitEdits?: never }) | SemanticWriterOptions) { + const { submit, submitEdits } = options; + if ((submit !== undefined) === (submitEdits !== undefined)) { + throw new MisoUsageError("ConsoleWriter requires exactly one of submit or submitEdits"); + } + if (typeof submitEdits === "function") this.#submit = submitEdits; + else if (typeof submit === "function") { + this.#submit = (edits) => submit(encodeLaneEdits(edits), edits.length); + } else { + throw new MisoUsageError("ConsoleWriter submission callback must be a function"); + } this.#maximumBatch = options.maximumBatch ?? ABI_LAYOUT.constants.defaultCommandQueueRecords; if (!Number.isInteger(this.#maximumBatch) || this.#maximumBatch < 1) { throw new MisoUsageError(`maximumBatch must be a positive integer`); @@ -260,7 +275,7 @@ export class ConsoleWriter { const edits = staged.map(([, edit]) => edit); this.#flushes += 1; - const report = await this.#submit(encodeLaneEdits(edits), edits.length); + const report = await this.#submit(edits); if (report.ok) { for (const [key, edit] of staged) { diff --git a/sdk/test/barrel-surface.ts b/sdk/test/barrel-surface.ts index 786ab1428..f4eb55cd2 100644 --- a/sdk/test/barrel-surface.ts +++ b/sdk/test/barrel-surface.ts @@ -154,3 +154,33 @@ export type BarrelSurfacePins = [ NoHeadlessCanonicalSessionJson, NoBrowserCanonicalSessionJson, ]; + + +// Both constructor modes share the writer; legacy annotated/extended options remain valid. +type SemanticWriterOptionsType = Assert>; +interface ExistingWriterOptions extends barrel.WriterOptions { readonly label?: string } +function writerSubmissionTypes(report: barrel.CommandReport) { + const encoded: ExistingWriterOptions = { submit: (_records, _count) => report }; + const legacyCallback: barrel.WriterOptions["submit"] = encoded.submit; + const semantic: barrel.SemanticWriterOptions = { maximumBatch: 4, submitEdits: async edits => { + // @ts-expect-error the selected batch is readonly + edits.push({}); + if (edits[0] !== undefined) { + // @ts-expect-error addressed edits are readonly + edits[0].trackIndex = 2; + // @ts-expect-error values are readonly + edits[0].values[0] = 1; + } + return report; + } }; + new barrel.ConsoleWriter(encoded); + new barrel.ConsoleWriter(semantic); + // @ts-expect-error both callbacks are ambiguous + new barrel.ConsoleWriter({ submit: legacyCallback, submitEdits: semantic.submitEdits }); + // @ts-expect-error one callback is required + new barrel.ConsoleWriter({ maximumBatch: 4 }); + // @ts-expect-error actual CommandReport is required + new barrel.ConsoleWriter({ submitEdits: async () => undefined }); +} +void writerSubmissionTypes; +export type _SemanticWriterOptionsType = SemanticWriterOptionsType; diff --git a/sdk/test/writer-evals.mjs b/sdk/test/writer-evals.mjs index 159f4c8af..b2ef2416d 100644 --- a/sdk/test/writer-evals.mjs +++ b/sdk/test/writer-evals.mjs @@ -434,11 +434,13 @@ describe("the writer contract -- the async submit boundary", () => { * The episode deliberately covers all three paths: admitted flushes, the refusal that fills a * paused queue, and the drain that lands the rest once the transport moves. */ - async function episode(wrap) { + async function episode(wrap, semantic = false) { const engine = await pausedEngine(); try { const writer = new ConsoleWriter({ - submit: wrap((records, count) => engine.submitCommands(records, count)), + ...(semantic + ? { submitEdits: edits => engine.console().submit(...edits) } + : { submit: wrap((records, count) => engine.submitCommands(records, count)) }), maximumBatch: 4, }); const outcomes = []; @@ -471,6 +473,7 @@ describe("the writer contract -- the async submit boundary", () => { test("an async submit produces the same outcomes and the same stats as a sync one", async () => { const sync = await episode(immediately); const async_ = await episode(nextMicrotask); + const semantic = await episode(undefined, true); // Guard the fixture itself: a transcript that never refused and never admitted would compare // equal for the wrong reason. @@ -481,6 +484,7 @@ describe("the writer contract -- the async submit boundary", () => { assert.deepEqual(async_.outcomes, sync.outcomes, "outcome sequences must not differ by timing"); assert.deepEqual(async_.stats, sync.stats, "stat sequences must not differ by timing"); assert.equal(async_.pending, sync.pending); + assert.deepEqual(semantic, sync, "semantic transport preserves actual admission, backpressure and coalescing"); }); test("two flushes entered without awaiting the first serialize into two disjoint batches", async () => { @@ -576,7 +580,7 @@ describe("the writer contract -- races the async boundary opens", () => { }); }; - test("an edit staged while its own batch is in flight is not deleted by that batch's success", async () => { + for (const semantic of [false, true]) test(`an edit staged while its own batch is in flight is not deleted by that batch's success (${semantic ? "semantic" : "encoded"})`, async () => { // The drag-during-round-trip case, which is the case the widening exists for. The hand does // not stop moving while a batch crosses the port: a newer value for an address already in // flight is staged before the report comes back. @@ -595,16 +599,15 @@ describe("the writer contract -- races the async boundary opens", () => { let held = false; const writer = new ConsoleWriter({ - submit: async (records, count) => { + ...(semantic ? { submitEdits: async edits => { + submissions.push(edits.map(edit => ({ address: `${edit.channel}/${edit.parameterId}`, value: edit.values[0] }))); + if (!held) { held = true; announceEntry(); await gate; } + return engine.console().submit(...edits); + } } : { submit: async (records, count) => { submissions.push(decode(records, count)); - if (!held) { - // Hold the FIRST submit open, so the stage below lands strictly inside the round trip. - held = true; - announceEntry(); - await gate; - } + if (!held) { held = true; announceEntry(); await gate; } return engine.submitCommands(records, count); - }, + } }), maximumBatch: 4, }); @@ -615,6 +618,8 @@ describe("the writer contract -- races the async boundary opens", () => { // The hand keeps moving while the batch is out. writer.stage(gainEdit(address, 0, -9.9)); + const queued = semantic ? writer.flush() : undefined; + assert.equal(submissions.length, 1, "a second flush cannot submit before the first report"); openGate(); const first = await inFlight; @@ -627,10 +632,10 @@ describe("the writer contract -- races the async boundary opens", () => { ); assert.equal(writer.pending, 1); - const second = await writer.flush(); + const second = await (queued ?? writer.flush()); assert.deepEqual( submissions[1], - [{ address: `0/${address}`, value: Math.fround(-9.9) }], + [{ address: `0/${address}`, value: semantic ? -9.9 : Math.fround(-9.9) }], "the value the hand actually reached goes out on the next flush", ); assert.equal(second.admitted, 1); @@ -651,7 +656,7 @@ describe("the writer contract -- races the async boundary opens", () => { } }); - test("an escalation rejects its own caller and does not poison later flushes", async () => { + for (const semantic of [false, true]) test(`an escalation rejects its own caller and does not poison later flushes (${semantic ? "semantic" : "encoded"})`, async () => { // The chain that serializes flushes is a promise, and a promise that is left rejected // propagates to everything chained behind it. An escalation must reject the call that caused // it and nothing else: the writer is as usable afterwards as a synchronous one, which throws @@ -665,7 +670,14 @@ describe("the writer contract -- races the async boundary opens", () => { let calls = 0; const corrupt = new Set([1, 3]); const writer = new ConsoleWriter({ - submit: async (records, count) => { + ...(semantic ? { submitEdits: async edits => { + await Promise.resolve(); + calls += 1; + const submitted = corrupt.has(calls) + ? edits.map((edit, index) => index === 0 ? { ...edit, trackIndex: 99 } : edit) + : edits; + return engine.console().submit(...submitted); + } } : { submit: async (records, count) => { await Promise.resolve(); calls += 1; if (corrupt.has(calls)) { @@ -673,7 +685,7 @@ describe("the writer contract -- races the async boundary opens", () => { .setUint32(at("trackIndex"), 99, true); } return engine.submitCommands(records, count); - }, + } }), maximumBatch: 1, }); @@ -749,3 +761,13 @@ describe("the writer contract -- races the async boundary opens", () => { } }); }); + + +test("writer construction requires exactly one callable submission path", () => { + const submit = () => { throw new Error("constructor must not submit"); }; + for (const options of [{}, { submit, submitEdits: submit }, { submit: 1 }, { submitEdits: null }]) { + assert.throws(() => new ConsoleWriter(options), MisoUsageError); + } + assert.doesNotThrow(() => new ConsoleWriter({ submit })); + assert.doesNotThrow(() => new ConsoleWriter({ submitEdits: submit })); +}); From 48c026b8da90368c853c9a94ae090c6e60e30671 Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 5 Sep 2026 15:47:02 +0900 Subject: [PATCH 3/4] docs(sdk): document semantic writer usage and validation --- ...its-through-the-existing-console-writer.md | 47 +++++++++++++++++++ sdk/README.md | 26 +++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md b/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md index 8cb1e52b5..633c1a1b2 100644 --- a/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md +++ b/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md @@ -133,3 +133,50 @@ A fresh independent Astra medium review follows the completed evidence, with root handling checkpoint pushes and GitHub synchronization. Matching issue: misofm/engine#445. Root approval: preserve existing WriterOptions interface, add the separate semantic option and one callback dispatch over the same queue. + + +## Attempt 1 implementation and evidence + +Astra medium implemented the approved slice. Root source checkpoint `edfe7431` +contains only `sdk/src/core/writer.ts`, `sdk/test/writer-evals.mjs` and +`sdk/test/barrel-surface.ts`. The existing exported `WriterOptions` interface +remains intact. `SemanticWriterOptions` extends its non-submit options and +provides `submitEdits(readonly LaneEdit[])`; the constructor excludes mixed +modes at both the type and runtime boundaries. + +Constructor dispatch normalizes the selected callback once. Only encoded mode +calls `encodeLaneEdits`; both modes continue through the same pending map, +serialized flush chain, batch selection, identity-based removal, adaptive +backpressure split and actual CommandReport handling. No additional writer, +queue, report synthesis, host protocol or app implementation was introduced. + +The existing real-engine paused episode now compares semantic async admission, +backpressure, drain outcomes and stats with encoded synchronous/asynchronous +submission. Existing in-flight latest-wins and non-flow refusal/recovery +fixtures run in both modes. The semantic in-flight case queues another flush +before the first report, confirms no early second submission, and observes the +unencoded addressed value on its later callback. Type proofs preserve legacy +annotated/extended WriterOptions while rejecting mixed/neither modes, mutable +semantic batches and callbacks without CommandReport results. + +Validation in `/private/tmp/miso-dx-sdk-writer`: + +- `bash scripts/check-sdk-types.sh`: PASS, including the existing host mirror + and root-barrel identities plus semantic/legacy constructor proofs. +- Artifact-backed `node --test sdk/test/writer-evals.mjs`: **16/16 PASS**; + log `/private/tmp/dx445-writer-focused.log`. +- `bash scripts/check-sdk-headless.sh /private/tmp/dx-393-current-artifacts`: + **163 PASS, 1 existing skip, 0 failures** (164 tests total); + log `/private/tmp/dx445-headless.log`. +- `bash scripts/sdk-package.sh check /private/tmp/dx-393-current-artifacts`: + PASS, including generated-policy checks and the existing fresh packed-consumer + gate (77 package files); log `/private/tmp/dx445-package.log`. + +All gates used the existing reviewed artifact directory. No Rust/Wasm rebuild, +generated artifact edit, dependency/package metadata change, new harness or +browser matrix occurred. README now documents both mutually exclusive callback +modes and their common queue/receipt behavior. App #101 can adopt submitEdits +and remove its already-listed encoded-record bridge independently. + +Final README/spec evidence awaits root checkpoint. A dedicated independent +Astra medium review is in progress; no independent verdict is claimed here. diff --git a/sdk/README.md b/sdk/README.md index beb5a3618..dbe298989 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -290,7 +290,31 @@ A refusal that is *not* flow control throws instead: backpressure succeeds on re thread drains, an unknown address never will, and retrying it silently would be an infinite loop wearing the costume of resilience. -`submit` may answer synchronously or with a `Promise` — in-process the engine answers immediately, +Choose exactly one submission callback. Encoded `submit(records, count)` keeps the existing +`WriterOptions` contract. Semantic `submitEdits(edits)` accepts the selected readonly `LaneEdit[]` +without an encode/decode round trip and is typed by `SemanticWriterOptions`: + +```ts +const console = engine.console(); +const writer = new ConsoleWriter({ + submitEdits: (edits) => console.submit(...edits), + maximumBatch: 32, +}); + +// Existing encoded integrations remain supported: +const encodedWriter = new ConsoleWriter({ + submit: (records, count) => engine.submitCommands(records, count), + maximumBatch: 32, +}); +``` + +Both callbacks return the actual `CommandReport` or a promise for it. Both use the same pending +map, batch sizing and serialized flush chain; semantic submission adds no queue. Providing both +callbacks or neither rejects. `maximumBatch` retains its existing default and positive-integer +validation. The callback owns transport-specific encoding and validation; `FlushOutcome` remains +the writer's admission/pending summary. + +`submit` and `submitEdits` may answer synchronously or with a `Promise` — in-process the engine answers immediately, but a browser host reaches it over a worklet port, where the answer is a promise by construction — so `flush()` and `drain()` are async. Flushes serialize: a call entered while a prior submit is still outstanding waits for it rather than picking its batch out of a map the earlier flush has not From 11c52271e1f686d15eeee6fce90107fcdec50b5e Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 5 Sep 2026 15:48:22 +0900 Subject: [PATCH 4/4] docs(sdk): record independent semantic writer pass --- ...it-semantic-edits-through-the-existing-console-writer.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md b/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md index 633c1a1b2..8789f5314 100644 --- a/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md +++ b/.github/ISSUE_SPECS/445-submit-semantic-edits-through-the-existing-console-writer.md @@ -180,3 +180,9 @@ and remove its already-listed encoded-record bridge independently. Final README/spec evidence awaits root checkpoint. A dedicated independent Astra medium review is in progress; no independent verdict is claimed here. + +## Independent review and delivery + +Dedicated independent Astra medium review records **PASS** at `48c026b8` (source `edfe7431`). Independent type checks, all **16** real-engine writer tests, and ordinary package/generated/fresh-consumer checks pass (77 package files). Review confirms one callback dispatch over the existing queue, unchanged admission/backpressure behavior, semantic edits without encoding, and legacy WriterOptions compatibility. Record: `/private/tmp/dx-445-astra-medium-review.md`. Author headless evidence is 163 PASS with one existing skip. + +This completes the SDK submission seam required to remove the app’s encoded-record bridge. App adoption and browser acceptance remain in misofm/app#101. No Rust, ABI, artifact or dependency changes.