Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ The SDK auto-discovers native binaries by checking `sdk/bin/<target-triple>/` (n

### Schema system

- **Stable schemas**: released, immutable schemas live in [`schemas/stable/`](../schemas/stable) (one file per released version, plus a `-strict` view) — never edit them after release.
- **Stable schemas**: released, immutable schemas live in [`schemas/stable/`](../schemas/stable) (one file per released version) — never edit them after release.
- **Dev schema**: the in-progress schema lives in [`schemas/dev/`](../schemas/dev). It is **generated** from the Rust wire model (`src/core/wxc_common/src/wire.rs`) by the `mxc_schema_gen` tool — **do not hand-edit it**. To change the dev schema, edit the wire model and regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- schemas/dev/mxc-config.schema.<dev>.json`. `scripts/versioning/check-schema-codegen.js` is a CI gate that regenerates and fails if the committed schema drifts. See [`docs/schema-codegen.md`](../docs/schema-codegen.md).
- **Generated SDK wire types**: `sdk/src/generated/wire.ts` is **generated** from the same wire model by the `mxc_schema_gen --ts` TypeScript emitter (`wxc_common::ts_emit`, no third-party generator) — **do not hand-edit it**. It is a drift oracle (not public API); the SDK unit test `sdk/tests/unit/wire-conformance.test.ts` asserts the hand-written public types in `sdk/src/types.ts` conform to it, and `scripts/versioning/check-sdk-types-codegen.js` is a CI gate that fails if the committed file drifts. Regenerate with `cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- --ts sdk/src/generated/wire.ts`.
- **Canonical schema-version source**: `schemas/schema-version.json` — the single source of truth for the schema-version constants (min/maxSupported/state-aware/stable/dev). `scripts/versioning/check-schema-versions.js` enforces that the Rust parser, SDK, and schema filenames all agree with it; do not hand-edit a schema-version constant without updating the canonical file. See [`docs/versioning.md`](../docs/versioning.md) for the full design.
- Config files can reference schemas via `"$schema"` for editor validation. `scripts/versioning/validate-configs.js` validates the `tests/examples` + `tests/configs` corpus against the dev schema in CI.

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/Versioning.Checks.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ jobs:
- name: Check schema is in sync with the Rust wire model (codegen)
run: node scripts/versioning/check-schema-codegen.js

- name: Check SDK wire types are in sync with the Rust wire model (codegen)
run: node scripts/versioning/check-sdk-types-codegen.js

- name: Validate config corpus against dev schema
run: node scripts/versioning/validate-configs.js
22 changes: 20 additions & 2 deletions docs/schema-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,30 @@ rules — gaps consciously owned by the parser. The equivalence is not a
one-time review: the codegen gate regenerates the schema from the types on every
CI run, and the corpus gate pins the accept-side behavior.

## Generated SDK types (drift oracle, Rust emitter)

The SDK's wire TypeScript types are generated too — by a **Rust emitter**, with
no third-party generator. `mxc_schema_gen --ts` walks the same generated schema
Comment on lines +107 to +108

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I'm still wondering why we can't have a third party generator? Wouldn't that ease our maintenance burden here? To be honest thinking about it, it makes a lot of sense if the types we expose in type script are generated from the wire rust code. I'm trying to think of a reason why we wouldn't want to do that but can't. Would be great if us devs only need to run a script to update the typescript types and then check in the changes.

side note: I wonder if we should have just used flatbuffers for our schema. It allows json conversion between the flatbuffer schema and json. Just wondering since that would have done the work for us in generating both rust types and typescript types without us making anything new.

value and `wxc_common::ts_emit` emits `sdk/src/generated/wire.ts`. That file is
**not public API** — it is a drift oracle. The unit test
`sdk/tests/unit/wire-conformance.test.ts` asserts (at `tsc` time) that the
hand-written public types in `sdk/src/types.ts` still conform to it, and
`check-sdk-types-codegen.js` is a CI gate (running the emitter and diffing the
committed file) that fails on drift. So a wire-model change ripples to all three
surfaces — Rust ⇄ schema ⇄ TS — and a forgotten SDK update fails CI instead of
drifting silently. The emitter handles only the JSON Schema constructs the MXC
schema uses (enums, closed/open objects, `$ref`, `anyOf [T, null]`, arrays,
scalars); extending the wire model with a new construct may require teaching the
emitter about it.

## Roadmap

- The wire model generates the committed dev schema, guarded by the codegen and
corpus CI gates.
- The parser deserializes directly into the wire model and the `Raw*` structs
are gone, so the schema source and the trust boundary share one definition of
the wire shape and cannot drift.
- Next: generate the SDK TypeScript types from the same schema and retire the
hand-maintained `*-strict.json` stable view.
- The SDK TypeScript wire types are generated from the same wire model
(`sdk/src/generated/wire.ts`, via the `wxc_common::ts_emit` Rust emitter),
guarded by a conformance test plus the `check-sdk-types-codegen.js` gate, and
the hand-maintained `*-strict.json` stable view has been retired.
250 changes: 0 additions & 250 deletions schemas/stable/mxc-config.schema.0.5.0-alpha-strict.json

This file was deleted.

71 changes: 71 additions & 0 deletions scripts/versioning/check-sdk-types-codegen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// SDK wire-types codegen gate (Phase 2C, option C): the committed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: we should probably remove mentions of the phases and options from the comments.

// `sdk/src/generated/wire.ts` must be identical (modulo line endings) to the
// output of the Rust TypeScript emitter (`mxc_schema_gen --ts`), so the SDK's
// drift oracle can never go stale relative to the Rust wire model. Unlike
// options A/B, this uses NO third-party generator — the emitter lives in
// `wxc_common::ts_emit`.
//
// Mirrors `check-schema-codegen.js`. Run from anywhere:
// node scripts/versioning/check-sdk-types-codegen.js

const { readFileSync, mkdtempSync, rmSync } = require("fs");
const { join } = require("path");
const os = require("os");
const { execFileSync } = require("child_process");

const repoRoot = join(__dirname, "..", "..");
const committedPath = join(repoRoot, "sdk", "src", "generated", "wire.ts");

function fail(msg) {
console.error("SDK wire-types codegen check FAILED:");
console.error(" - " + msg);
process.exit(1);
}

let committed;
try {
committed = readFileSync(committedPath, "utf8");
} catch (e) {
fail(`could not read committed ${committedPath}: ${e.message}`);
}

const tmpDir = mkdtempSync(join(os.tmpdir(), "mxc-ts-emit-"));
const tmpOut = join(tmpDir, "wire.ts");
try {
// Build + run the emitter. Quiet so only our diagnostics surface.
execFileSync(
"cargo",
["run", "-q", "-p", "mxc_schema_gen", "--", "--ts", tmpOut],
{ cwd: join(repoRoot, "src"), stdio: ["ignore", "ignore", "inherit"] }
);
const generated = readFileSync(tmpOut, "utf8");

// Compare modulo line endings: the file is committed with LF, but a Windows
// checkout with core.autocrlf=true has CRLF in the working tree. The emitter
// always writes LF.
const normalize = (s) => s.replace(/\r\n/g, "\n");
if (normalize(generated) !== normalize(committed)) {
const g = normalize(generated).split("\n");
const c = normalize(committed).split("\n");
let line = 0;
while (line < g.length && line < c.length && g[line] === c[line]) line++;
fail(
`committed SDK wire types are stale at ${committedPath}.\n` +
` First difference at line ${line + 1}:\n` +
` committed: ${JSON.stringify(c[line])}\n` +
` generated: ${JSON.stringify(g[line])}\n` +
` Regenerate with (from the repo root; the Cargo workspace is in src/):\n` +
` cargo run --manifest-path src/Cargo.toml -p mxc_schema_gen -- --ts sdk/src/generated/wire.ts`
);
}
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}

console.log(
"SDK wire-types codegen OK: committed sdk/src/generated/wire.ts matches the Rust emitter output."
);
3 changes: 1 addition & 2 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ child.on('close', (code) => console.log('exit:', code));
| Version | Status | Schema file |
| --- | --- | --- |
| `0.4.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.4.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.4.0-alpha.json) |
| `0.5.0-alpha` | Stable (legacy — see strict sibling below) | [`schemas/stable/mxc-config.schema.0.5.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.5.0-alpha.json) |
| `0.5.0-alpha` (strict) | Stable, non-experimental surface only | [`schemas/stable/mxc-config.schema.0.5.0-alpha-strict.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.5.0-alpha-strict.json) |
| `0.5.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.5.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.5.0-alpha.json) |
| `0.6.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.6.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.6.0-alpha.json) |
| `0.7.0-alpha` | Stable (current) | [`schemas/stable/mxc-config.schema.0.7.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.7.0-alpha.json) |
| `0.8.0-alpha` | Dev (experimental backends, the `experimental.*` block, state-aware sandbox lifecycle) | [`schemas/dev/mxc-config.schema.0.8.0-dev.json`](https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json) |
Expand Down
2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"watch": "tsc --watch",
"clean": "rimraf dist",
"test": "npm run test:unit",
"test:unit": "npm run build:test-unit && node --test dist-tests/tests/unit/sandbox.test.js dist-tests/tests/unit/policy.test.js dist-tests/tests/unit/logger.test.js dist-tests/tests/unit/errors.test.js dist-tests/tests/unit/state-aware-types.test.js dist-tests/tests/unit/state-aware.test.js dist-tests/tests/unit/platform.test.js",
"test:unit": "npm run build:test-unit && node --test dist-tests/tests/unit/sandbox.test.js dist-tests/tests/unit/policy.test.js dist-tests/tests/unit/logger.test.js dist-tests/tests/unit/errors.test.js dist-tests/tests/unit/state-aware-types.test.js dist-tests/tests/unit/state-aware.test.js dist-tests/tests/unit/platform.test.js dist-tests/tests/unit/wire-conformance.test.js",
"test:integration": "cd tests/integration && npm install && npm run build && npm test",
"prepublishOnly": "npm run build"
},
Expand Down
Loading