Skip to content

Take the model layer to @typra/emitter 0.4.30 and green all five runtimes - #456

Open
Seth Juarez (sethjuarez) wants to merge 70 commits into
mainfrom
wip/typra-0.4.20-regen
Open

Take the model layer to @typra/emitter 0.4.30 and green all five runtimes#456
Seth Juarez (sethjuarez) wants to merge 70 commits into
mainfrom
wip/typra-0.4.20-regen

Conversation

@sethjuarez

@sethjuarez Seth Juarez (sethjuarez) commented Aug 6, 2026

Copy link
Copy Markdown
Member

Moves the generated model layer from @typra/emitter 0.4.19 to 0.4.30 and takes all five runtimes to zero failures.

Measured at 752d80c7

runtime command result
Rust cd runtime/rust && cargo test --workspace --no-fail-fast 1147 passed / 0 failed
C# cd runtime/csharp && dotnet test prompty.sln 1356 passed / 0 failed (46 skipped)
TypeScript cd runtime/typescript && npm test 1734 passed / 0 failed (52 skipped)
Python pytest tests/ -q in a clean venv 1484 passed / 0 failed (17 skipped)
Go go test -count=1 ./... PASS, 0 failed

5721 passing, 0 failing. CI: 34/34 checks green.

Note on commands: Rust and TypeScript are multi-package. cargo test -p prompty (908) and vitest in packages/core (1534) each miss the three provider crates/workspaces — roughly 445 tests covering the OpenAI, Foundry and Anthropic wire layers. Use the workspace-level commands above.

Notable fixes in this branch

The Python package was uninstallable from a clean environment. pyproject.toml uses dynamic = ["version"], so flit resolves __version__ by loading __init__.py via spec_from_file_location — which executes the package without putting its parent on sys.path. Four generated runtime-cancellable ports carried an absolute self-import alongside relative sibling imports, so uv pip install -e . died with ModuleNotFoundError: No module named 'prompty'. Any Python measurement taken before this was made in a stale virtualenv. Fixed with one line of emit config (cancellation-token-path), no emitter release required.

A second CI blocker was masked behind the first. The install failure always killed the job before ruff format --check ran, so formatting drift had accumulated invisibly.

The schema reproducibility gate had never passed — including on the commit that introduced it. Generation shells out to rustfmt, prettier/eslint and ruff, and each degrades to a warning when absent, producing unformatted output indistinguishable from stale generated code. The gate now installs all three, fails fast on formatter-missing warnings, and pins UV_FROZEN=1 so the tracked uv.lock cannot be silently rewritten and reported as staleness.

On the emitter bumps

0.4.28, 0.4.29 and 0.4.30 each changed zero generated source files here — their fixes target constructs prompty's schema does not express (bare float/numeric, Swift/Java toWire, C# fixture generation). The 0.4.30 bump commit touches only 3 files, and that is the expected signature of a fully-executed regeneration against a release that is a no-op for our emit targets — not a skipped regeneration. The repro-check gate now distinguishes the two automatically.

Not verified

  • Live-provider integration tests were not re-run at this exact commit (deselected by default; exercised earlier in the branch).
  • Four cross-runtime behaviours are deliberately left unlegislated as owner decisions, not defects: required-scalar save divergence; 4.0 classified integer by Go and float by Rust; Python subtype-load inconsistency (_Property.py:136); Anthropic bare {"type":"array"} divergence.

Seth Juarez (sethjuarez) and others added 30 commits August 4, 2026 03:27
Define the schema-owned engine ports, runtime effect metadata, deterministic acceptance vector, and legacy harness preservation gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Thread generated executor cancellation through the pipeline and provider SDK calls, with forwarding and pre-cancellation coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extend the canonical acceptance vector so PortError cannot become a generated model or wire export.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pin exact metadata, ordered parameters, wire exclusions, and complete native signatures for every configured target.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Open the Connection discriminator and pin exact forward-compatible payload preservation with shared known, unknown, and case-collision vectors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise generated Connection load/save APIs against the shared forward-compatibility vectors without editing generated runtime code.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Join TextPart values with newlines in Python and TypeScript and return an empty string for empty TypeScript messages, matching the canonical contract and Rust behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document ContentPart as a closed, case-sensitive union and add shared known, unknown, and wrong-case acceptance vectors. Add a non-generated Rust gate that exposes the current generated fallback-to-text defect without modifying generated code.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add native public-API gates for the shared Connection load-save-reload vectors across C#, Go, Python, Rust, and TypeScript.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add native public-API gates for the shared strict ContentPart vectors across C#, Go, Python, Rust, and TypeScript.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Make the canonical tools_function_load vector assert exact binding map keys and input values across C#, Go, Python, Rust, and TypeScript.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Define direct Property scalar coercion separately from named input shorthand and add one atomic shared vector across C#, Go, Python, Rust, and TypeScript.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Define collection-context scalar shorthand precedence and add focused string, integer, float, and boolean vectors with a Rust no-degradation gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Consume the shared Record<unknown> vectors in C# and assert exact model-field coverage plus load/save/reload preservation of direct, nested, and list nulls.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Assert the canonical IDictionary<string, object?> mapping and outer optionality for all nine shared Record<unknown> vector surfaces.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…iminator

Connection is an open discriminator: `.prompty` files may carry connection
kinds this schema has never heard of, and load -> save -> reload must return
the payload byte-for-byte, including the exact `kind` string and every
undeclared top-level key.

The previous shape could not do that:

    alias ConnectionType = "remote" | "reference" | ... ;
    model Connection { kind: ConnectionType; }

Unknown kinds silently collapsed to "reference" (the first variant), failing
spec/vectors/model/connection_roundtrip_vectors.json 1 pass / 2 fail.

This commit changes `kind` to a bare `string` and keeps @abstract and
@Discriminator("kind"). That is the whole fix.

Why not a wildcard subtype
--------------------------
The obvious alternative - mirroring Tool's `union ToolTypes { ..., string }`
plus `model CustomTool extends Tool { kind: "*" }` - was implemented and
measured, and it is wrong here. It preserves the discriminator exactly
(including case), but a wildcard subtype serializes only *declared* fields:
to_value() re-emits `kind` plus the base fields and structurally drops every
undeclared key. Connection vectors carry arbitrary extras (endpoint, tenant,
priority, regions, providerOptions, ...), so that shape still failed 2 of 3.

Bare `kind: string` on an @abstract base gives full raw-payload passthrough,
which is what the vector's exact-equality requirement needs. This matches the
emitter's own fixtures/rust-unknown/main.tsp, which is a near-exact replica of
this shape.

Also note @abstract does NOT need to be dropped - an earlier proposal paired
"kind: string" with removing @abstract. Only the first half was necessary.

Measured (Rust, connection_roundtrip_vectors):
  alias + no wildcard  -> 1 pass / 2 fail (kinds collapse to "reference")
  union + kind: "*"    -> 1 pass / 2 fail (kind exact, extras dropped)
  bare kind: string    -> 3 pass / 0 fail

This commit is schema-only and does not include regenerated models.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
*** THIS COMMIT IS NOT REPRODUCIBLE FROM package.json. DO NOT MERGE AS-IS. ***

package.json here pins @typra/emitter 0.4.15, but this output was NOT produced
by published 0.4.15. It was produced by a locally built, unpublished 0.4.18
carrying the two-line IR fix on the typra branch:

    wip/nonabstract-base-discriminator-fallback  (typra, commit d870296)

It was installed with `npm install --no-save`, so schema/node_modules holds
bytes that no version number describes. Anyone who runs `npm ci` and
regenerates WILL get materially different output than what is committed here.

To reproduce: build that typra branch, `npm pack` it, install the tarball into
schema/, then regenerate. Or, better, wait for a real published release and
regenerate from a clean `npm ci`.

Why the emitter needed patching at all
--------------------------------------
Bumping 0.4.2 -> 0.4.15 to pick up open-discriminator handling also picked up a
regression in *concrete* polymorphic bases. Prompty's `Property` is a
non-@abstract model with @Discriminator("kind") whose union permits scalar
kinds (string, integer, float, boolean, thread, audio) that no subtype claims.
0.4.2 emitted a self-referencing fallback for those; 0.4.15+ emits a panic!
plus a validate_discriminator() that rejects anything outside
array|object|union. Every scalar-kind Property therefore panics on load.

This is the same defect class reported earlier against Go's
model/property.go:LoadProperty, which returned a zero Property for exactly
these kinds. It is a shared emitter bug, not a per-language one.

Verification status - read this before trusting anything here
--------------------------------------------------------------
Rust:   296/0 at 0.4.2 (clean baseline)
        289/7 at published 0.4.15
        286/10 at published 0.4.18
        293/3 with the patched emitter used for this commit
        The 3 remaining failures (model_invocation_request.rs:45,
        tool.rs:115 x2) are NOT investigated.
Go / C# / Python / TypeScript / Java / Swift: NOT BUILT, NOT TESTED against
        this regeneration. 0.4.15 is a breaking change - optional collections
        became Option<Vec<T>> / equivalent - so handwritten seams in every
        runtime are expected to break the same way Rust's did.
Typra's own test suite: NOT RUN against the patch.

Committed to preserve the work for manual triage, not because it is ready.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…>> change

Emitter 0.4.15 is a breaking change for optional collections: fields that were
`Vec<T>` are now `Option<Vec<T>>`. Prompty's `inputs` / `outputs` moved;
`tools` did not, because it carries a `#[]` default.

Two handwritten sites had to follow:

  src/model_ext.rs
    as_inputs() / as_outputs() now go through
    self.inputs.as_ref().filter(|items| !items.is_empty())
    preserving the previous "empty collection reads as absent" semantics.

  tests/named_collection_vectors.rs
    two assertions rebound loaded_inputs / reloaded_inputs via .as_ref().

This is exactly the class of break to expect in the other six runtimes, none
of which have been built against this regeneration. Anyone picking this up
should grep each runtime for direct field access on inputs/outputs first.

Scope note: this repairs compilation and the collection semantics only. It does
not address the 3 remaining Rust test failures (model_invocation_request.rs:45,
tool.rs:115 x2), which are uninvestigated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…curring

Prepends a mandatory "Operating Rules" section to .github/copilot-instructions.md.
Placed at the top rather than appended because the file is ~68KB of rebuild plan
and anything at the end is not read in practice.

Each rule is derived from a specific failure in the seven-runtime parity effort,
which deadlocked for roughly 24 hours and had to be abandoned:

1. Vectors are the only authority - a "must load as the base type" constraint
   circulated for a day and drove an emitter-release escalation. It exists in no
   vector. Constraints must be quoted from spec/vectors/** or labeled an opinion.
2. Green baseline before bumping codegen - the 0.4.2 -> 0.4.15 bump regressed
   Rust 296/0 to 289/7, but no baseline had been recorded, so the regression was
   initially misread as pre-existing.
3. Measure before escalating - the entire deadlock was an argument about which
   emitter version could be published. No publish was needed. Three schema shapes
   A/B'd afterward settled it in under an hour.
4. Report numbers, not impressions, and name what was NOT verified.
5. Orchestration budget - two exchanges on one blocker, then pull it in-house.
   Also records that archive_session is parent-only and idle != stopped.
6. On "stop", commit to wip/ branches, split for cherry-picking, and flag any
   state not reproducible from the manifest.
7. Repo facts that cost hours to rediscover: origin fails SAML 403 (use ssh),
   origin/* refs go stale, npm versions are immutable, the Connection
   open-discriminator shape, and the non-abstract-base fallback rule.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bumps the emitter pin from 0.4.15 to 0.4.20 and regenerates all backends.
0.4.20 is the first release cut from typra `main` -- every version from
0.4.3 through 0.4.18 was published from an unmerged branch, which is why
`main` sat at 0.4.2 while npm `latest` reported 0.4.18.

Emitter fixes included in this bump:

  - non-abstract polymorphic base now absorbs discriminator values that no
    subtype claims, instead of returning a zero value (Go) or panicking
    (Rust). This is what `Property` relies on: it is concrete and its union
    permits string/integer/float/boolean/thread/audio, none of which have
    subtypes.
  - open discriminators are no longer pre-validated against their declared
    union before dispatch, which had made the open fallback unreachable.
  - Go scalar coercions now bridge decoder-native numeric types.
    `encoding/json` yields float64 for *every* JSON number, so the emitted
    `case int:` / `case float32:` arms matched nothing and fell through to a
    zero value.
  - generated tests now synthesize payloads for required complex fields that
    carry no @sample, so a generated test can pass its own generated
    validation.
  - Go abstract open-discriminator bases preserve unknown kinds and their
    payloads rather than erroring.
  - load diagnostics now report array element indices.

VERIFIED

  typra npm test              290 passed / 0 failed
  typra validate:fixtures     pass
  npm run generate            "Compilation completed successfully."

  runtime/go/prompty          go test ./...   62 failures -> 1
  runtime/rust/prompty        cargo test      293 passed / 3 failed
  runtime/typescript (core)   vitest run      67 failures -> 14

NOT VERIFIED

  Python runtime does not build (`uv pip install -e ".[dev,all]"` fails with
  ModuleNotFoundError: No module named 'prompty' inside its own build
  isolation), so its suite was never run. C# was not run either.

KNOWN REMAINING FAILURES, all pre-existing and attributed

  TypeScript, 6 of 14 -- emitter defect, tracked as sethjuarez/typra#59:
  TypeScript/C#/Python still conflate abstract with closed in polymorphic
  dispatch, so Connection rejects unknown kinds:
      Error: Unknown Connection discriminator field 'kind' value: future-auth
  This violates spec/vectors/model/connection_roundtrip_vectors.json. The Go
  half of this landed as typra#58; the other three backends did not.

  Go 1 of 1, and TypeScript 1 of 14 -- prompty handwritten seam, not emitter:
  turn_runner's permission-denied branch returns HostToolResult without first
  recording TurnEventTypeToolResult, so the golden replay journal is missing
      turn:tool_result:0:add:false:permission_denied
  violating spec/vectors/harness/replay_vectors.json.

  Rust 3 of 3 -- prompty test data, not emitter: tool_dispatch.rs:1044 and
  :1065 omit `connection`; a live_turn test omits `context`.

  TypeScript, remaining 7 -- not yet attributed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`ReferenceTurnRunner`'s permission-denied branch built a `HostToolResult`
and returned it without ever recording a `tool_result` turn event, so the
denial never reached the event sink or the journal. Any consumer replaying
the journal saw a permission decision with no corresponding outcome.

`spec/vectors/harness/replay_vectors.json` is explicit about the expected
sequence for the `permission_denied` scenario:

    turn:permission_requested:0:exec-1-permission
    turn:permission_completed:0:false
    turn:tool_result:0:add:false:permission_denied
    turn:messages_updated:0

Note there is deliberately no `tool_execution_start` / `tool_execution_complete`
pair -- the tool is never executed -- but the result is still recorded. The
approved path already did this (Go `turn_runner.go:245`, TypeScript
`turn-runner.ts:201`); only the denied path was missing it.

Fixed in both runtimes by binding the result to a local, recording
`tool_result`, and then returning it. Go additionally propagates a recording
error rather than swallowing it, matching every other `recordTurn` call in
that function.

Rust already recorded this event correctly and needed no change.

VERIFIED

  runtime/go/prompty        go test ./...                     1 failure -> 0
  runtime/typescript core   vitest tests/harness/turn-runner   6 passed / 0 failed
  runtime/typescript core   vitest run                       14 failures -> 13

Both suites were failing on this exact assertion before the change and pass
after it, with no other test changing state in either direction.

NOT VERIFIED

  Python and C# runtimes were not run. Python does not currently build.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
spec/vectors/load/load_vectors.json, vector `empty_frontmatter_body_only`,
requires that a .prompty carrying only a name loads successfully:

    "input":    { "frontmatter": { "name": "empty-fm" } },
    "expected": { "kind": "prompt", "name": "empty-fm",
                  "model": null, "inputs": null, "tools": null }

The vector asks for a null model -- not a defaulted one -- but agent.tsp
declared `model: Model | string` as required, so any model-less prompt was
rejected at load with "model: missing required field". Every sibling field
(inputs, outputs, tools) is already optional; model was the lone outlier.

This is a defect in prompty's own TypeSpec, not in the emitter. The emitter
was correctly enforcing what the schema declared.

Schema edit only -- regeneration follows in the next commit so the source
change and the generated churn stay independently reviewable and revertable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Regeneration only -- no hand edits. Produced by `npm run generate` from
schema/ against @typra/emitter 0.4.20, picking up the previous commit's
change making `Prompty.model` optional.

Every backend now emits model as a nullable/optional field:

  rust     model: Option<Model>
  go       Model *Model
  ts       model?: Model | string
  csharp   Model?
  python   model: Model | None

The JSON AST and the VS Code schema follow.

Handwritten call sites that assumed a non-optional model break as a result;
those repairs are in the following commit, kept separate so the generated
churn stays independently reviewable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`Prompty.model` became `Option<Model>`, so seven handwritten sites that
reached through it no longer compile. Adapted rather than unwrapped blindly:

  pipeline.rs resolve_provider / is_streaming
      thread the Option through with as_ref().and_then(..); a model-less
      prompt now falls back to DEFAULT_PROVIDER and non-streaming instead
      of being unrepresentable.

  pipeline.rs trace payload
      model.id becomes null in the trace when absent rather than "".

  live_turn.rs model_id
      map-then-filter, preserving the previous "empty id means None" rule.

  tests/load_vectors.rs
      This one is a genuine strengthening. It previously inferred a null
      model from an empty id:

          // expected null model -> id should be empty (default)
          assert!(agent.model.id.is_empty(), ..)

      which is not what the vector asks for. load_vectors.json's
      `empty_frontmatter_body_only` states "model": null, and that is now
      directly assertable, so it asserts agent.model.is_none().

  tests/loader_test.rs, tests/live_turn_execution.rs
      Fixtures here all declare a model; added a single model_of() helper
      instead of scattering .as_ref().unwrap() across thirteen assertions.

MEASURED, cargo test --no-fail-fast, whole target set

  before   39 failed
  after    36 failed

Nothing regressed; three tests newly pass (loader_test +1,
named_collection_vectors +1, record_unknown_nullability_vectors 0/1 -> 1/0).

A NOTE ON THE BASELINE

Earlier runs in this effort reported Rust as "293 passed / 3 failed". That
number was wrong. Plain `cargo test` stops at the first failing target, so
it only ever reported the lib target and silently skipped every integration
test binary. The true figure is above. Use --no-fail-fast for any Rust
measurement in this repo.

The 36 remaining failures are NOT caused by this change and are unrelated to
Prompty.model. The largest block is tests/agent_vectors.rs, 0 passed / 28
failed, all panicking identically:

    tools.parameters.properties: invalid named collection entry category array

spec/vectors/agent/agent_vectors.json supplies properties in list form:

    "parameters": { "properties": [ { "name": "city", "kind": "string" } ] }

which spec/vectors/model/named_collection_vectors.json declares canonical --
its own first vector loads "inputs" in exactly that shape, and its header says
array-valued entries are rejected only "in name-keyed object form", while
"arrays in declared entry fields remain valid". The emitted validator is
rejecting the legal collection-level list form. Tracked separately; this is an
emitter defect, not a prompty one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
structured.prompty declared an array output with no items:

    - name: keyPoints
      kind: array
      required: true

schema/model/core/properties.tsp makes items required on ArrayProperty --
`items: Property | Named<..>` with no `?` -- so the loader correctly
rejected the fixture with "outputs[1].items: missing required field". The
fixture was violating the schema it exists to exercise; the emitter was right.

(The indexed `outputs[1]` path in that message is the array-index reporting
from typra #47 working as intended.)

MEASURED, packages/core, npx vitest run

  tests/loader.test.ts structured output   4 failed -> 0 failed

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 02:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Extends commit 233ef47 (which legislated per-provider ModelOptions
omission for Anthropic in Rust and TypeScript) to the Python and C#
wire runners. Test-only; no runtime source changed in any language.

I reported to the typra session that "Python and C# run only the OpenAI
side of wire_vectors.json". That claim was WRONG. Both dispatch the
Anthropic vectors:

  runtime/python/prompty/tests/test_spec_vectors.py:671
      provider == "anthropic" -> _anthropic_build_chat_args
  runtime/csharp/Prompty.OpenAI.Tests/SpecVectorWireTests.cs:393
      AnthropicChatVectors()

So both already picked up the new anthropic_unmapped_options vector on
the previous commit -- and both PASSED it while measurably blind.

Measured blindness. Both are pure delegations to the generated
to_wire/ToWire with no allow-list, i.e. the most exposed consumers of
the omission rule:

  python  providers/anthropic/executor.py:133  opts.to_wire("anthropic")
  csharp  Prompty.Anthropic/AnthropicExecutor.cs:122  opts.ToWire("anthropic")

Simulating typra #84 at each of those seams (leak fields that carry no
provider mapping, which is the defect's actual trigger):

  python  28 passed /  0 failed   <- fully blind
  csharp   6 passed /  0 failed   <- fully blind, no extra-key check at all

Root cause per runner:
  python  three hand-enumerated absence checks (tools, output_config,
          system) -- whack-a-mole, one line per key, so any new leaked
          key is invisible. Replaced with one general
          set(actual) - set(expected) check.
  csharp  Anthropic_Chat_WireFormat iterated expected keys only and had
          no extra-key assertion whatsoever. Added one.

Sensitivity after the fix, same simulations:
  python   1 failed / 27 passed   <- only anthropic_unmapped_options
  csharp   1 failed /  5 passed   <- error names frequencyPenalty,
                                     presencePenalty, seed exactly

Cost of the tightening on clean trees: zero.
  python  28 / 0
  csharp   6 / 0

Full suites, after simulations removed and git diff on every src/
confirmed empty:
  cd runtime/python/prompty && uv run pytest tests/ -q
      1483 passed / 0 failed (17 skipped)   baseline 1482, +1 = new vector
  cd runtime/csharp && dotnet test prompty.sln --nologo
      1355 passed / 0 failed                baseline 1354, +1 = new vector
      (Core 1112, OpenAI 209, Anthropic 21, Foundry 13)

Consumer-side enforcement of the omission rule now exists in four
runtimes: Rust, TypeScript, Python, C#.

Not verified / known gaps:
- Go remains structurally uncovered. runtime/go/prompty contains only
  model/ -- there are no provider crates and no wire runner at all.
  This is not fixable by a test; it needs providers to exist.
- Only the Anthropic path is tightened. The OpenAI wire runners in both
  languages were not audited for the same blindness in this commit.
- The Rust wire_test! list in prompty-anthropic/tests/vectors.rs is
  still hand-maintained, so new vectors there are opt-in rather than
  discovered. Unchanged by this commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 02:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Follow-up to f07df49, which legislated typra #84's rule (a ModelOptions
field with no wire mapping for a provider must be omitted, never emitted
under its schema field name) on the Anthropic surface. That commit's
"not verified" note said only the Anthropic path had been tightened.
This attacks that note rather than leaving it.

The wire map (model/_ModelOptions.py:149-163, mirrored in every backend)
determines exposure, and it is not uniform:

  openai      maps all 9 options   -> structurally immune
  responses   maps 3 of 9          -> 6 unmapped, fully exposed
  anthropic   maps 5 of 9          -> 4 unmapped, exposed

The Responses surface was a second, wholly unlegislated exposure. All
three pre-existing responses vectors set ZERO options, so the surface had
no option coverage of any kind -- despite pure-delegation builders that
call the generated to_wire directly (Rust prompty-openai/src/wire.rs:661,
Python providers/openai/executor.py:329, C# WireFormat).

New vector `responses_unmapped_options` sets the three mapped options plus
all six unmapped ones and expects only model, input, temperature,
max_output_tokens, top_p -- a positive and a negative assertion in one case.
Registered in Rust's hand-maintained wire_test! list; TS guard counts bumped.

Also generalises the Python checker. f07df49 fixed only the Anthropic
checker; auditing the other four found the same defect class in all of
them -- _check_wire_chat and _check_wire_responses hand-enumerated two keys
each, and _check_wire_embedding and _check_wire_image had no extra-key
check at all. All five now share _extra_key_errors().

Sensitivity, measured by simulating #84 in the *generated* to_wire, scoped
to the responses provider string (a blanket leak breaks several vectors and
proves nothing about incremental coverage):

  Rust      1 failed / 26 passed, sole failure responses_unmapped_options;
            all three pre-existing responses vectors pass under the defect
  Python A  tightened checkers + defect    -> 1 failed / 28
  Python B  HEAD checkers + defect + new vector -> 29 / 0, FULLY BLIND

So the vector and the checker tightening are jointly necessary; neither
alone catches this.

Tightening the Python OpenAI-chat checker also surfaced a live gap: under
a blanket defect the tightened suite reports 2 failures against HEAD's 1,
and the delta is options_additional_properties -- the chat path was blind
to additionalProperties leaking into every request.

C# is structurally immune on the OpenAI and Responses surfaces, and this
is documented in the source (WireFormat.cs:137-138): those paths never call
ToWire, they assign each option onto typed SDK properties, so there is no
dictionary for an unmapped key to leak into. Confirmed by measurement --
with the defect applied, Responses reported 4/0 before the check and 53/0
after. The assertion added here is therefore a forward guard against a
refactor onto the ToWire path, not a live catch. C# does delegate for
Anthropic, which f07df49 already covered.

Cost: zero. Python 28/0 -> 29/0, C# OpenAI 209/0 -> 210/0.

Measured after:
  rust        cargo test --workspace --no-fail-fast   1147 / 0
  typescript  npm test                                1734 / 0
  csharp      dotnet test prompty.sln                 1356 / 0
  go          go test ./...                           ok
  docs lint                                           2 pre-existing failures (unchanged)

Not verified:
- Go is structurally uncovered: runtime/go/prompty contains only model/,
  no providers and no wire runner. Not fixable by a test.
- TypeScript was not simulated for the responses defect. Its toEqual-based
  comparison suggests it would catch it, but that is reasoning, not a
  measurement.
- C# chat/embedding/image extra-key checks: chat now carries the same
  forward guard, embedding and image do not.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 03:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

A clean regeneration against the pinned emitter reported 291 modified
files. Investigated before acting, per the rule about attributing a diff
before writing a fix.

Finding: not staleness. The Rust emitter does not format its output, but
the committed tree is rustfmt-formatted. The entire diff was line-joining
and trailing commas -- 13,329 deletions against 4,038 insertions, all of
it collapsing multi-line expressions onto single lines. Every other
language regenerated byte-identically; 157 of the 157 files that differed
under a whitespace-insensitive comparison were `runtime/rust/prompty`.

Proved it by running `cargo fmt -p prompty` on the regenerated output:
`git status` went clean. So the committed generated code is exactly
`rustfmt(emitter 0.4.27 output)` -- fully reproducible, nothing stale.

This is a trap worth removing rather than documenting. A 291-file phantom
diff on a clean regeneration hides real diffs, and the next person to
bump the emitter has to rediscover that none of it is theirs.

normalize-typra-output.mjs already exists for exactly this purpose and
already handles two cases -- empty Python test files, Go trailing
whitespace. Rust formatting is the missing third, and by far the largest.

Scoped to `-p prompty` deliberately: the Rust workspace also contains the
handwritten provider crates (prompty-openai, prompty-anthropic, and the
harness), which this script has no business reformatting. Degrades with a
warning rather than failing the build when no Rust toolchain is present,
since the other emitted languages should still generate.

Verified: `npm run generate` on a clean tree now leaves zero modified
files. Before this change the same command produced 291. The working tree
is byte-identical to HEAD, which measured `cargo test --workspace
--no-fail-fast` -> 1147 / 0, so no re-run is implied by this commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 03:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Nothing enforced this, and the gap is demonstrable in this branch's own
history. Two commits bumped the emitter pin without regenerating:

    00a040f  chore(model): bump @typra/emitter to 0.4.26   3 files
    38c7a5a  chore(model): bump @typra/emitter to 0.4.27   3 files

Neither touched generated source; the last full regeneration was 4494d96
at 0.4.25. Both ran **zero CI**, because every runtime workflow is
paths-filtered to `runtime/<lang>/**` and there is no workflow for
`schema/**` at all. A schema-only commit triggers nothing.

Both turned out to be no-ops -- I verified by regenerating at the pinned
0.4.27, which produced 0 modified and 0 deleted files, proving 0.4.25 and
0.4.27 output are byte-identical for prompty's target set (and, since
there were no deletions, that 0.4.26's pruning had no orphans to remove).
But that was luck, not a gate. The same pattern with a release that did
change output would have shipped stale generated code silently, and the
only symptom would be runtime failures attributed to the wrong cause.

The check is now trivially expressible because the preceding commit made
`npm run generate` idempotent: regenerate, assert `git status` is clean.
Before that it reported ~291 phantom files and could not have been a gate.

rustfmt is installed deliberately. The Rust emitter does not format its
output while the committed tree is formatted, so without that component
normalize-typra-output.mjs degrades to a warning and this job fails on a
pure line-joining diff. Same reason the job pins a real Rust toolchain
rather than only Node.

Verified:
- YAML parses; 3 triggers, 6 steps
- On the current tree the assertion passes (measured: `npm run generate`
  -> 0 modified files)
- Revert-check: appending one line to a generated file makes
  `git status --porcelain` non-empty, so the gate fires. Reverted clean.

Not verified: this has not executed on a GitHub runner. `npm ci` in
schema/ and the tsp compile are assumed to work there; both run clean
locally, and node_modules is gitignored (confirmed -- the local zero-diff
run had it present).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 03:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Ships typra #88 (issue #87): array-element diagnostics lost the element
index in the Rust, C# and Swift backends. Loading a collection whose Nth
entry fails validation reported the bare collection path, so with ten
entries every failure produced an identical diagnostic:

  before   inputs.detail
  after    inputs[1].detail

TypeScript, Go, Python and Java already threaded the index; the contract
from typra #47 had reached only four of seven backends.

Regeneration touches Rust (30 files) and C# (33 files) only, +303 -147.
The generated change is uniform: collection loops become
`for (index, entry) in entries.iter().enumerate()` with the index folded
into the path passed to the element validator. Swift is not an emit
target here, so it is not represented.

Bump and regeneration are committed together deliberately. The schema
reproducibility gate added in 9ce13c1 fails any commit where generated
output does not match the pinned emitter, so a bump-only commit -- the
shape of 00a040f and 38c7a5a, both of which silently shipped stale
output -- is no longer a valid intermediate state.

Verified, full suites, before -> after:

  rust        cargo test --workspace --no-fail-fast   1147/0 -> 1147/0
  csharp      dotnet test prompty.sln                 1356/0 -> 1356/0
  typescript  npm test                                1734/0 -> 1734/0
  go          go test ./...                           ok     -> ok
  python      uv run pytest tests/ -q                 1484/0 -> 1484/0

typra flagged one risk: any test asserting an exact error message
containing a collection path would now see `field[i].sub`. Searched the
Rust, C#, Python, TypeScript and Go trees for diagnostic-string
assertions before bumping and found none -- the only message assertion
matching the shape checks a filename (`resolve.rs:318`). The zero delta
across all five runtimes confirms it: prompty's vectors assert that a
load fails, not the path it reports.

That is also why no consumer could have caught this defect, and why the
fix is worth taking despite moving no numbers. It was found by asserting
the #47 contract uniformly across typra's own conformance runners.

Regeneration remains idempotent at 0.4.28: a second `npm run generate`
leaves the tree unchanged (66 files both runs), so the reproducibility
gate stays a reliable staleness signal.

Not verified: Swift and Java emitters, which prompty does not generate.
The diagnostic path change itself is exercised only by typra's fixtures;
no prompty vector asserts a diagnostic string, by design.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 03:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Upstream fix: typra #89 — the Rust backend honoured @entryShorthand on load
but never on save, writing a name-keyed collection entry in expanded form
where the other six backends wrote the shorthand scalar.

Generated delta is small and Rust-only: 3 source files plus the export
surface manifest, +30 lines. The new code is a guarded save-side branch:

  if ctx.use_shorthand && item_data.len() == 1 { ... }

Measured — every runtime matches the 0.4.28 baseline exactly, zero delta:

  Rust        cd runtime/rust && cargo test --workspace --no-fail-fast
              1147 passed / 0 failed
  C#          cd runtime/csharp && dotnet test prompty.sln --nologo
              1356 passed / 0 failed (46 skipped)
  TypeScript  cd runtime/typescript && npm test
              1734 passed / 0 failed (17 skipped)
  Python      cd runtime/python/prompty && uv run pytest tests/ -q
              1484 passed / 0 failed (17 skipped)
  Go          cd runtime/go/prompty && go test -count=1 ./...
              ok

Pin and regeneration travel together in one commit, as with 0.4.28. The
schema-repro-check workflow regenerates from the pinned emitter and fails on
a dirty tree, so a bump without regeneration is not a valid intermediate
state. This is a deliberate deviation from the separate-commits rule.

Verified but NOT fixed here — recorded so it is not rediscovered:

The new save-side shorthand is unreachable in prompty, and its target field
disagrees with the load side.

  1. Reachability. The gate requires item_data.len() == 1. Property always
     serialises `kind` and `enumValues` (as "" and [] when unset), so the
     length is never 1. A probe over four input shapes produced no shorthand
     save in either Rust or Python; both emit byte-identical expanded form:

       {"city": "Seattle"}            -> {"kind":"string","default":"Seattle","enumValues":[]}
       {"city": {"example": "..."}}   -> {"kind":"","example":"Seattle","enumValues":[]}

     So prompty cannot serve as the regression lock for typra #89.

  2. Target mismatch. schema/model/core/properties.tsp:15 declares
     @entryShorthand("default"), and loading a shorthand scalar does place
     the value in `default` — asserted by the
     string_scalar_in_name_keyed_inputs_infers_property vector. But every
     backend's save-side shorthand reads `example`, which is the @Coerce
     target from properties.tsp:18/24/31/37, not the @entryShorthand target:

       Rust  model/core/property.rs   item_data.get("example")
       Go    model/property.go        copy["example"]
       TS    core/property.ts:8       shorthandProperty = "example"
       C#    core/Property.cs:29      ShorthandProperty => "example"

     Uniform across backends, so this is not a cross-runtime divergence — but
     load and save name different fields. Were the length gate ever satisfied,
     a Property carrying only `example` would save as a bare scalar and reload
     with that value in `default`. Reported upstream; not actionable here
     while unreachable.

Also verified: regeneration is idempotent at this pin (a second
`npm run generate` leaves the tree clean), and the pin is exact rather than a
caret so `npm ci` cannot silently absorb a future publish.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 05:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

The Python package could not be installed from a clean environment. This
was not a CI flake; `uv pip install -e .` failed outright, and every green
Python number this branch has reported was measured in a virtualenv that
predated the generated pipeline ports.

Reproduced before changing anything, in a fresh venv:

    uv venv <tmp> && uv pip install --python <tmp>/Scripts/python -e .
    -> File ".../prompty/model/pipeline/_EnginePermissionPort.py", line 10
         from prompty.core.cancellation import CancellationToken
       ModuleNotFoundError: No module named 'prompty'

Mechanism: pyproject declares `dynamic = ["version"]`, so flit resolves
__version__ by loading prompty/__init__.py through spec_from_file_location.
That executes the package without putting its parent directory on sys.path,
so relative imports resolve and absolute self-imports do not. The four
generated runtime-cancellable ports were the only absolute self-imports in
the package:

    model/pipeline/_EnginePermissionPort.py
    model/pipeline/_EnginePostCommitPort.py
    model/pipeline/_EngineToolPort.py
    model/pipeline/_Executor.py

Each already imported its generated siblings relatively (`from ..agent._Prompty
import Prompty`) and only the cancellation token absolutely, so the file was
internally inconsistent — the same two-halves-disagreeing shape as typra #78,
#87 and #89.

Fix is configuration, not an emitter change. `cancellationTokenPath` is an
existing emit-target option; the emitter only splits it on the last dot
(python/emitter.js:107-118), so a relative dotted path passes through intact.
Generated ports live at prompty/model/pipeline/, so `...` is the package root.

Not escalated to typra, because the cheap experiment settled it: one config
line and a regeneration, versus an emitter release. Worth recording for them
anyway that the default is absolute and that the adjacent context import in
the same function is already depth-aware (`group ? ".." : "."`) while the
cancellation import is not — so the default will keep breaking any consumer
whose packaging imports the module. That is an upstream defect; it is simply
not one we need a release for.

Verified in a clean venv, which is the environment that previously failed:

    uv pip install -e ".[dev,all]"   ok
    python -c "import prompty"       ok (2.0.0b3)
    ruff check .                     All checks passed
    python -m pytest tests/ -q       1484 passed, 17 skipped, 0 failed

Same 1484 as before, but now reproducible from the manifest rather than from
a pre-existing local virtualenv.

Regeneration remains idempotent: a second `npm run generate` leaves the tree
clean, so the reproducibility gate still holds. Schema config and its
regeneration travel together for that reason.

Not verified: Rust, C#, TypeScript and Go were not re-run. This change is
scoped to the Python emit target and touches no other runtime's output.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`ruff format --check .` reported two files, which fails the Python CI's
formatting step. The failure was previously masked: the install step ran
first and died on the absolute cancellation import, so this step never
executed and the drift accumulated unnoticed.

    prompty/providers/openai/executor.py   two call sites re-joined onto one line
    README.md                              python blocks in fenced examples

Both changes are cosmetic. The executor diff is line-joining only, no
semantic change:

    props[p.name] = _property_to_json_schema(p, optional=..., strict=strict)
    tool_def["parameters"] = _schema_to_wire(tool.parameters, strict=...)

Neither file is generated, so this is a handwritten-seam repair and is kept
in its own commit, separate from the schema change and its regeneration.

Verified in the same clean venv used for the install fix:

    ruff check .           All checks passed
    ruff format --check .  368 files already formatted

Not verified: tests were not re-run for this commit. `ruff format` makes no
semantic change, and the full suite (1484 passed / 0 failed) was measured on
the parent commit with these same two files already formatted.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The schema reproducibility gate has never passed. It failed on all three
commits it has ever run against, including 9ce13c1, the commit that added
it. I added a gate and never confirmed it went green — the exact baseline
failure the operating rules warn about.

    gh run list --workflow schema-repro-check.yml --branch wip/typra-0.4.20-regen
      bf0bb00  failure
      6c78733  failure
      9ce13c1  failure   <- introduced the gate

CI reported 547 files changed / 3351 insertions / 6576 deletions while local
regeneration was clean. Root cause is in the job log, not inferred:

    Warning: prettier not found for .../typescript/packages/core/src/model
    Warning: prettier not found for .../typescript/packages/core/tests/model
    Warning: ruff check failed  for .../python/prompty/prompty/model
    Warning: ruff format failed for .../python/prompty/prompty/model

Generation shells out to three formatters, and each degrades to a warning
when absent, producing unformatted output that is indistinguishable from
stale generated code in a diff:

    rustfmt   schema/scripts/normalize-typra-output.mjs:27 -> cargo fmt -p prompty
    prettier  emitter typescript/driver.js:125 walks up for
              node_modules/prettier/bin/prettier.cjs, then npx eslint --fix
    ruff      emitter python/driver.js:137 -> uv run ruff check --fix
              and uv run ruff format, from the Python project root

Only rustfmt was installed. Locally prettier resolves from
runtime/typescript/node_modules and ruff from runtime/python/prompty/.venv,
which is why nothing reproduced on a developer machine.

Changes:

  - npm ci in runtime/typescript, providing prettier and eslint
  - astral-sh/setup-uv, plus uv venv and an editable [dev] install in
    runtime/python/prompty, providing ruff
  - the regenerate step tees to $RUNNER_TEMP/generate.log and greps for the
    formatter-missing warnings, failing with the real cause rather than
    letting the next step report false staleness. The log lives outside the
    repo deliberately: a log file under schema/ would itself appear in
    git status --porcelain and fail the assertion it exists to explain.
  - UV_FROZEN=1 at job level. runtime/python/prompty/uv.lock is tracked and
    the emitter invokes `uv run`, which would otherwise re-lock and dirty the
    tree — again reporting as generated-code staleness. Frozen turns that into
    an explicit lockfile error instead.

Both installed trees are ignored, so neither can trip the assertion:
runtime/python/prompty/.gitignore:126 (.venv) and .gitignore:4 (node_modules/).

NOT VERIFIED, and this is the important caveat: nothing local can confirm
this fix. The gate's failure mode is specific to the CI environment, so the
only proof is a green run. If it still fails, read the log for which
formatter is still missing rather than guessing again — that is how this was
found, and guessing is what let it sit red for three commits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Regeneration produces no change to any generated source file. The only
delta is the version stamp in the generated manifest:

  schema/tsp-output/.typra-generated/export-surfaces.json  0.4.29 -> 0.4.30

This is the expected outcome, not a skipped regeneration. 0.4.30 fixes
three C# backend defects that prompty cannot reach:

  - `float` and `numeric` had no entry in the C# type map, and `number`
    mapped to 32-bit `float`. prompty declares neither bare `float` nor
    `numeric` anywhere in schema/model -- only explicit float32/float64
    (verified by grep; the sole `numeric` occurrence is in a comment at
    core/properties.tsp:130). So no prompty field changes type.
  - Generated C# conversion tests asserted members that do not exist on
    polymorphic bases. prompty's generated conversion tests were already
    passing and none of them changed.
  - Generated C# factory tests asserted raw templates. Same -- no diff.

Worth recording, because a file count was previously read as evidence
that a bump had skipped regeneration: a fully-executed `npm run generate`
against a release that touches none of our emit targets modifies exactly
three files -- package.json, package-lock.json, and the two-line version
stamp in export-surfaces.json. That signature means "regenerated, and the
release was a no-op for us". It does not distinguish itself from a
skipped regeneration by file count alone; only by running the generator
and observing a clean tree afterwards, which is what the schema
reproducibility gate now enforces on every commit.

Verified at this commit:
  cd runtime/csharp && dotnet test prompty.sln
    -> 1356 passed / 0 failed (46 skipped), identical to 0.4.29
  npm run generate -> clean, no formatter-missing warnings
  second `npm run generate` -> idempotent, no further diff

Not verified at this commit: Rust, TypeScript, Python and Go were not
re-run. No generated source changed, so no behavioural change is
possible in any runtime; the schema reproducibility gate and the
per-runtime CI jobs cover this on the PR.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 06:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

}

// LoadCompactionConfig creates a CompactionConfig from a map[string]interface{}
func LoadCompactionConfig(data interface{}, ctx *LoadContext) (CompactionConfig, error) {
if ctx == nil {
ctx = NewLoadContext()
@sethjuarez Seth Juarez (sethjuarez) changed the title Take the model layer to @typra/emitter 0.4.26 and green all five runtimes Take the model layer to @typra/emitter 0.4.30 and green all five runtimes Aug 6, 2026
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.

2 participants