Skip to content

feat(metadata): declare how an encoder batches, and check that it can - #2009

Merged
justinchuby merged 18 commits into
mainfrom
justinchuby/vision-batching-schema
Aug 25, 2026
Merged

justinchuby merged 18 commits into
mainfrom
justinchuby/vision-batching-schema

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 24, 2026 •

Copy link
Copy Markdown
Owner

Do not merge

Paused behind the design of record, PR #2010. This is the schema half; that PR
is the contract half. Coordinated against justinchuby/vision-batching-design
at 0ea8344a48d494b27740bf133ff0af57ecc50653, and docs/genai/ENCODER_BATCHING.md
at that SHA is authoritative wherever this branch and that doc disagree.

What this adds

A component may declare how many requests one invocation of it can absorb, and a
tensor may say which of its entries are real. Nothing here executes anything --
the runtime work is downstream. Every package that does not spell token_packed
parses unchanged, because absence of batch_capacity still means one request per
invocation; token_packed itself is reshaped and its old spelling is refused by
name, which the "Breaking change" section below states in full.

  • WorkflowComponent.batch_capacity -- uniform_dimensions (shape symbols that
    must agree across the requests sharing an invocation) and budgets (bounds on
    what the invocation may materialize).
  • TensorContract.padding -- a list of { dimension, valid_lengths } entries
    naming the value that says how much of a padded dimension is real.
  • BatchLayout::TokenPacked { axis, levels } -- a flat payload plus a bounded
    chain of ownership levels, each { offsets, owner, extent }, innermost first.
  • Vision content vocabulary for the companions themselves: pack_offsets,
    pack_owner, and the generic valid_lengths role.

Video is not a separate mechanism. A frame-to-clip-to-request chain is two
levels of the same ownership construct, and the fixtures cover both a temporally
padded encoder and a packed one.

Second review, resolved

  1. extent is per level, not per layout. A chain may mix: the canonical
    video fixture produces its inner token-to-clip companions and preserves the
    caller's outer clip-to-row pair. An output level omitting extent is
    rejected; an input level declaring one is rejected too, since the runtime
    built those. preserved is checked positively -- the pair must belong to an
    input port of the same component and agree with it on both extent symbols.

  2. A version gate that runs before typed deserialization. parse_metadata
    is a real entry point, and the loaders that used to call serde_yaml
    directly now call it -- schema_version.rs proves both the file path and the
    in-memory path go through it. Grammar is [v]major[.minor] with minor
    defaulting to 0, so absent, v1, 1, and 1.0 are one version. A document
    whose minor exceeds what the runtime supports is refused with its version as
    written, its normalized form, and the highest supported version.

    Deliberately stricter than crates/onnx-model-package/src/lib.rs:563-579,
    which parses a minor and then ignores it. §6.1 of the design justifies the
    divergence: without a gate, a v1.1 document meets deny_unknown_fields on an
    old core and fails with a message about a field name rather than a version.

  3. uniform_dimensions names ordinary per-item symbols only -- never a
    port's flattened packed symbol, never an ownership level's unit count. Both
    of those are counts the scheduler chose, and pinning them would be a bound
    pretending to be a shape fact.

This revision

Corrections from the coordination round, each a rule that was right about the
wrong quantity.

  • Budgets are group-rooted. The previous revision rejected a singleton
    budget only when its symbol was pinned. Pinning is not the property that makes
    a bound meaningless -- failing to reach the group is. patches alone counts
    patches per frame, the same number for one frame or a thousand, pinned or
    padded. dimensions is now a nesting path read outermost first, and its first
    symbol must be a count the scheduler chose. A per-item symbol may appear in
    later positions, where it multiplies a count into a footprint.
  • A fixed frame count keeps the clip-to-row level. It drops the frame level
    only. FIXED_FRAME_VIDEO_ENCODER is [clips, frames, patches, features]
    packed over clips, frames an ordinary pinned dimension. A request-aligned
    alternative would pad the clip axis, which is the cost packing exists to avoid.
  • SCHEMA_VERSION stays v1. Normalizing on read is what makes the
    spellings in the wild interchangeable; changing what writers stamp would
    rewrite bytes to say something no reader distinguishes. The comfyui golden
    document is byte-identical to main. v1.1 is what a batching document
    declares, 1.1 is an accepted synonym, and SchemaVersion displays with the
    v so an error names a version the way a document would write it. A present
    but non-string schema_version is refused by the gate rather than deferred.
  • Padding names its length by name, not by role. An audio program points a
    padding entry at the frame_lengths it already emits. LENGTH_CONTENT_ROLES
    is informative only, and valid_frames / valid_samples left it -- those are
    counts a graph consumes, not statements about padding.
  • Pair identity resolves both extents. Every port naming a companion pair
    must agree on the offsets extent and the owner extent, not merely name the
    same two values. Identity is the pair, never the level index: the video
    fixture names one pair at level 1 of the pixel port and level 0 of the pooled
    embedding port.
  • The serving carve-out is narrow. A shared emitted value is admitted only
    when it is int64, carries the rank the reference demands, and is named as an
    offsets or owner of another emitted value's layout -- or as the
    valid_lengths of another emitted value's padding entry -- in the same
    workflow. The rank is read from the reference: offsets and owner maps are
    rank 1 by construction, while a length vector's rank is the number of axes
    outer to the padded one (rank == axis). No new BatchLayout variant.
    A declared owner is internal -- an input named as one may not be
    externally_suppliable, since invocation-global positions leak cross-request
    identity, while offsets stay suppliable because the runtime rebases them.

Code review, resolved

Three defects, each a rule reaching further than its reason.

  • A shipped package did not expire. The rank-4 append rule refused the
    checked-in diffusion and diffusion_guided packages, which append a latent
    [batch, 4, height, width] along its final axis. They are correct and always
    were: the final axis is what the default meant, and they had no way to say so
    explicitly because nothing asked. The rule's reason is that a rank-4 media
    tensor makes that axis ambiguous, and a rank-4 media tensor is what v1.1
    introduced -- so the demand starts at BATCHING_SCHEMA_VERSION. An older
    document keeps the default it was written against; a document declaring v1.1
    names the axis; an axis outside the value's rank names nothing at any version
    and is still rejected.

    Version scoping means the validator has to be told the version, so
    validate_pipeline_spec now takes one. Deriving it from the document's fields
    would be the same fact stored twice, and defaulting it inside the validator
    would make strictness depend on which entry point a caller happened to use.
    Both fixtures are migrated to explicit axis: 3 regardless -- stating the axis
    the default already chose costs nothing and outlives the exemption.

    On attribution: the append-axis rule is a state-append rule of this crate, not
    an encoder-batching contract, and docs(metadata): design of record for generic component batching #2010 does not legislate it -- nothing here
    cites the design as its authority. What the design did adopt from this round
    is the general form of the split, now §6.1: a version may gate what a document
    must say, but it never gates what a document may say wrongly. A demand
    to state intent explicitly starts at the version that made the default
    ambiguous, because a document written earlier meant the default it was written
    against; range checks and every rule about a value a document did state stay
    unconditional, because relaxing those would let an older spelling assert
    something false rather than merely leave something unsaid. The two look alike
    in a validator and are opposites in effect, so validate_emit_axis now quotes
    the principle rather than restating the one rule.

  • A padded output could hide the only account of its padding. The serving
    rule that every emitted rank>0 value declare a row correspondence admitted
    ownership offsets and owner maps, because those are shared by construction.
    A padded output's valid_lengths is shared for the same reason -- one entry
    per position outside the dimension it bounds -- and was rejected. The carve-out
    now names both and stays decidable from the declared outputs alone: an
    unrelated shared result is still a per-request answer with no way back to a
    request.

    The other half of that symmetry was missing too. validate_packed_emit_companions
    required a packed emit to publish the companions that split it; nothing
    required a padded emit to publish the lengths that read it. A caller holding
    trailing entries that mean nothing, with no way to learn how many, is the
    failure the padding contract exists to prevent -- and the schema deliberately
    refuses a payload-shaped validity mask as the alternative, so that vector is
    the only account there is. §4 rule 8 is now retitled for both halves -- a
    ragged emit publishes the account of its raggedness, and serving admits it --
    and matrix row 25 carried the four cases this branch tested at that round: a
    packed emit with its companions, a padded emit with its lengths, a padded emit
    withholding them (rejected), and an unrelated shared result (still
    rejected). Row 25 and this branch have both grown since; the round below
    records what the flat rank-1 reading cost and what replaced it.

    The three companions then differ on delivery, and the difference is now pinned
    by a test rather than left implicit. An owner map is invocation-global and is
    never delivered. Offsets are per-request but group-relative, so they are
    delivered rebased to the request's own start. A valid_lengths is already
    relative to the item it measures and means the same number in any group, so a
    request states its own and receives the slice indexing its own items -- nothing
    to rebase and no group position to leak. Only the owner rule constrains
    suppliability; the test exists so that a later attempt to unify the delivered
    companions with the internal one fails rather than passes quietly.

  • A user-facing error was malformed. The unquoted-schema_version message
    was missing its line continuations and reached a reader with fourteen-space
    runs in the middle of two sentences.

Two extensions this branch proposed, since adopted

Both were carried past the doc deliberately, recorded here rather than assumed,
and both are now rules in the design of record at 0ea8344a4. They are stated
as rules with their own tests, not left as coverage that happens to exist.

  1. The packed axis is 0 wherever token_packed appears, not only in a
    component that declares batch_capacity. The doc originally scoped the rule
    to capacity components; the scope was the bug. The justification it gives --
    a no-copy view is a contiguous element window
    (crates/onnx-genai-ort/src/value.rs:1526-1542) -- holds whenever a runtime
    splits a packed result per request, and the emit path rebases offsets and
    derives per-request owners with no capacity in sight. Under the old scope a
    strided packed emit was legal exactly where nothing had declared it could pay
    for one. Now §4 rule 2, decisions §10.5 / §10.6, matrix row 17.

  2. A preprocessing-program companion carries the pack_offsets or
    pack_owner content role.
    Now §4 rule 4, with the scope made explicit:
    the requirement reaches only declarations that have a role. A program says
    what each of its outputs is, so one wiring some other plausible int64 rank-1
    vector into a levels entry is caught at load rather than at the first
    split; a companion the graph produces is an ONNX output port and has nowhere
    to carry a role, so an extent: produced level is not asked for something
    its half of the schema does not have. The validator was already scoped this
    way and a test now pins it.

    §3.3 records why padding deliberately does not get the same treatment, so
    the two are not read as an inconsistency: length vectors already have
    established modality spellings (frame_lengths, sample_lengths) that
    programs legitimately emit, so no single role could be demanded without
    breaking a program that predates the generic name, while the two ownership
    roles are new and reject nothing that exists. Both rules resolve the
    reference by name; only this one constrains what the referenced
    declaration may claim to be.

Code review, round two: companions are not all rank one

The serving carve-out asked whether an emitted shared value was named as some
declared output's companion and whether it was int64 rank 1. That is right
for an ownership offsets or owner map, which are rank 1 by construction, and
wrong for a validity length. A length has one entry per position of the axes
outer to the dimension it bounds, so its rank is that dimension's axis index:
rank 1 for a padded axis 1, rank 2 for a padded axis 2. A correct rank-2 length
was refused as "a per-request value without a declared batch_layout" -- advice
that, if followed, would have made it wrong.

The bare name set is now a map from companion name to what the declarations
naming it require: the role, and the rank that role implies at that site. The
carve-out stays narrow -- int64, referenced by a declared output contract, and
at the exact rank that reference demands.

The design doc adopted the correction rather than the code working around it.
#2010 now words the carve-out as int64 at the rank that reference demands,
and records why the flat form was wrong: a refusal under it necessarily advised
request_aligned, which is the one layout a companion MUST NOT declare, so
following the doc would have made a correct package wrong. The three judgement
calls below are normative there now.

  • A name may be claimed by several declarations, so expectations are collected
    rather than overwritten and a value satisfies the carve-out by matching any one
    of them. Two declarations demanding different ranks of one value is a
    contradiction, but it is validate_padding's to report against the declaration
    that is wrong -- not this rule's to report as a missing row axis.
  • Where a padded dimension does not resolve to an axis the expectation carries no
    rank, because a second complaint about a rank nothing could compute sends a
    reader to the wrong line.
  • A value named as a companion but shaped like something else now gets that as
    its error, naming the role and rank expected of it. The generic message told
    its author to declare a row axis, which is precisely what a companion must not
    do.

Both companion obligations now require the companion to be emitted, not merely
declared.
Declaring an output and writing it are different facts and only the
second delivers anything: a workflow that declares its lengths and never emits
them satisfies the letter of "publishes them" while handing the caller a padded
tensor and an empty vector, which is the failure the obligation exists to
prevent. The same hole existed on the packing side and is closed with it -- one
rule, two spellings. The check is whole-workflow rather than path-sensitive,
deliberately: whether two emits reach a caller together depends on a branch
predicate, and this crate does not evaluate predicates, so a path-sensitive rule
would either reject correct packages whose companion is written in a sibling
branch or claim a precision it does not have. "Somewhere in the declared steps"
is what can be decided soundly, and it catches the case that occurs.

One narrowing found while aligning with the design's wording. The carve-out
asked that the naming output be declared; the companion obligation asks that a
companion be emitted. Those are two answers to one question, and the gap is
exploitable -- a package could carry any shared vector past the serving rule by
declaring a padded output beside it and never emitting one. An expectation now
carries the output that claims it and whether any step writes that output, and
only a written claim admits anything. This is also what the design says: a
companion is admitted when named by another emitted value's layout or padding
entry. A value left out by this is told so, rather than told to declare a row
axis -- the defect is the payload's missing emit, not the companion.

Seven tests over two new fixtures -- a request_aligned and a token_packed
serving output, each padded on two dimensions with a rank-1 length for axis 1 and
a rank-2 length for axis 2: valid publication of both shapes; a packed result
padded on the axes it does not pack, publishing ownership and lengths together at
three different ranks; a length declared at rank 1 for a padded axis 2, rejected
by role and rank; a declared-but-never-emitted length; a declared-but-never-
emitted owner map; and an unrelated shared rank-2 result, still rejected.

Breaking change: the flat token_packed spelling

token_packed on main carries offsets and owner directly on the layout,
which can say only that items sit in request rows. This PR replaces that with
levels, an ownership chain, because an item can itself be a group -- frames in
clips in rows -- and one pair cannot say that. A document written against the
flat spelling does not load, at any declared version.

This is deliberate and unshimmed. A one-level chain is exactly what the flat
pair meant, so a translation would be expressible; it is refused instead because
the schema is pre-release and rule 3 is to reshape completely rather
than keep a second spelling alive, and because no in-tree package uses
token_packed at all -- nothing ships that a translation would rescue.

What a clean break owes is the error, and that debt was unpaid until now. serde
alone reported unknown field offsets, expected axisorlevels``, which
reads like a typo and stops at the contract rather than the layout, so a reader
goes hunting for a misspelling instead of finding a migration.
parser::reject_flat_token_packed recognizes the retired shape in the preparse
-- the same place and the same discipline as `reject_retired_model_io`, which
never produces a value and only ever produces a refusal -- and reports the
containing path, the replacement to write, and that nothing will convert the
document for it:

the `token_packed` batch layout at
`pipeline.workflow.inputs.image_pixels.contract.batch_layout` declares `offsets`
and `owner` directly, which is the retired flat spelling of ownership. A packed
axis now declares `levels`, an ownership chain innermost first, because an item
can itself be a group. The flat pair is the one-level case: replace `offsets:
<o>, owner: <w>` with `levels: [{ offsets: <o>, owner: <w> }]`, and add an outer
entry if the items are themselves grouped. An emitted level also states
`extent`. This spelling is not read, and no document is converted for you.

It runs after the version gate. The flat pair is a reshape inside the v1
line, so refusing it presumes the document belongs to that line; a document
declaring a version this build cannot read is refused for that instead, because a
reader that has never seen v2 has no standing to say what a spelling means there.
A test pins the order rather than the outcome: both orders reject a v2.0
document carrying the flat pair, and only the message distinguishes them, so
a_document_from_a_version_this_build_cannot_read_is_refused_for_that asserts
the migration text is absent and the version is named.

A noted inconsistency, deliberately not fixed here. reject_retired_model_io
runs before the gate; this one runs after. I am not aligning them in this PR.
Whether pre-gate is wrong for that recognizer depends on what its message
asserts -- if the shape it refuses was retired across the major boundary rather
than inside a line, running first may be correct for it -- and that is a fact to
be established by reading it, not inherited from this one. Reordering a shipped
refusal path is a behavioural change that deserves a PR whose evidence is about
that path, rather than arriving as a side effect of adding a second recognizer.

Two compatibility claims were overstated and are corrected rather than left to
be discovered. "A package that uses nothing new keeps loading" is a statement
about fields that were added; it was written as though it covered everything,
and a flat-token_packed document uses nothing new and no longer loads. The
schema_version field doc and the schema_version.rs module doc now say so and
point at the refusal. The published schema needed no change -- token_packed
already requires levels and sets additionalProperties: false, so a producer
validating against the schema alone is told the same thing the loader tells it --
but that agreement now has a test rather than holding by accident, mirroring the
existing model.io requirement that the two readers not disagree about what is
loadable.

Seven tests in encoder_batching.rs, one in schema_sync.rs: the refusal names
the shape, the path, and the finality; it fires at v1, "1.0", v1.0 and
v1.1 alike; a half-migrated layout keeping one of the two keys is still
explained rather than dropped back to the unknown-field error; the migrated
document the error prints actually loads; a value merely named offsets is not
mistaken for a retired layout; a v2.0 document is refused for its version and
not lectured about a v1 migration; and the published schema publishes neither
retired key.

A padded output has one account of how much of it is real

An emit may carry a valid_length, and an output contract may declare
padding. Both say how much of a result is real. Nothing reconciles them, and
a document setting both on the same value is now refused at load.

They are not two views of one fact. valid_length truncates what a step writes
and is invisible in the contract -- a caller reading the metadata cannot see it.
A padding entry is part of what the caller reads and names a length vector
that rule 8 requires the workflow to publish. So a document that sets both can
ship a tensor whose published lengths describe a shape the emit did not
produce, and the caller holds the account the emit is free to contradict. Two
answers to one question, which is the same defect RULES.md rule 10 names.

padding.valid_lengths is authoritative, and the error says so, because
"these two disagree" is not actionable until the reader knows which one to
delete. It names the emit's valid_length, the output, and every padded
dimension with the length vector bounding it:

<step path> emits 'image_features' into output 'features' with valid_length
'accepted_len', but 'features' declares padding whose valid_lengths
'tile_lengths' on dimension 'max_tiles' already says how much of it is real; a
padded output has one account of its raggedness and it is the declared padding,
which is the only one the caller receives, so drop the emit's valid_length

token_packed needs no case of its own and gets none. A packed layout declares
no request axis, so an emit-level length into one is already refused by the
existing rule -- ragged emission with no row axis to be ragged along. The two
rules answer different questions, so a packed and padded output hears from
both, which is correct rather than redundant.

Nor is this version-scoped, and it does not need to be. padding is itself a
1.1 field, so any document able to express the contradiction has already been
required to declare v1.1 by the existing feature detector; a v1 document
cannot reach this rule. And it is a rule about what a document did state, not
a demand that it state something new -- the half of the validate_emit_axis
split that stays unconditional at every version.

Three tests, one fixture pair: the rejection, the same workflow with the
emit-level length removed accepted as its control (guarded so it cannot pass
vacuously), and the packed case still refused for the older reason (guarded with
assert_ne! so a non-matching edit cannot make it vacuous either). The
rejection was verified by mutation -- disabling the rule fails it.

Not in scope

Runtime execution, scheduling, and admission. Load-time validation checks the
graph and the contracts; whether a particular tensor's offsets are monotonic is
an invocation-time check on the device that holds them, and this branch does not
force a copy to look.

Verification

  • cargo test -p onnx-genai-metadata -- 342 across 25 binaries (encoder_batching 102, schema_version 13, schema_sync 3).
  • cargo test -p onnx-genai-engine --test onnx_genai_workflow_conformance -- 15/15,
    including mobius_euler_diffusion_workflow_executes_complete_path,
    mobius_euler_diffusion_workflow_executes_batched,
    mobius_guided_diffusion_workflow_executes_complete_path, and
    mobius_video_diffusion_workflow_publishes_causal_temporal_chunks.
  • validate_metadata binary on tests/fixtures/onnx_genai_workflows/{diffusion,diffusion_guided,video} -- all valid.
  • cargo test -p onnx-genai-genai-config -p onnx-genai-preprocess -p onnx-genai-comfyui-config, cargo test -p onnx-genai-engine --lib (494), cargo check -p onnx-genai-server --all-targets.
  • schema/inference_metadata.schema.json regenerated and in sync.
  • cargo fmt --all --check, cargo clippy -p onnx-genai-metadata --all-targets -- -D warnings.

@justinchuby
justinchuby force-pushed the justinchuby/vision-batching-schema branch from 6312bc9 to 3f6b368 Compare August 24, 2026 16:36
@codecov

codecov Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.92453% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.27%. Comparing base (54625db) to head (9151a37).
⚠️ Report is 32 commits behind head on main.

Files with missing lines Patch % Lines
crates/onnx-genai-metadata/src/parser.rs 85.39% 8 Missing and 5 partials ⚠️
crates/onnx-genai-metadata/src/schema/ir.rs 75.00% 10 Missing ⚠️
crates/onnx-genai-preprocess/src/image/program.rs 0.00% 6 Missing ⚠️
crates/onnx-genai-comfyui-config/src/lib.rs 50.00% 0 Missing and 1 partial ⚠️
...rates/onnx-genai-genai-config/src/compatibility.rs 0.00% 1 Missing ⚠️
crates/onnx-genai-metadata/src/version.rs 99.05% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2009      +/-   ##
==========================================
+ Coverage   80.31%   81.27%   +0.96%     
==========================================
  Files         409      426      +17     
  Lines      190562   211589   +21027     
  Branches   190562   211589   +21027     
==========================================
+ Hits       153046   171969   +18923     
- Misses      32082    33940    +1858     
- Partials     5434     5680     +246     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (?)
cli-ort-windows 72.01% <ø> (?)
mlas 85.33% <ø> (?)
offline 81.43% <87.92%> (+1.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/onnx-genai-comfyui-config/src/lower.rs 98.78% <100.00%> (+<0.01%) ⬆️
crates/onnx-genai-metadata/src/decoder_workflow.rs 95.27% <100.00%> (+0.04%) ⬆️
crates/onnx-genai-metadata/src/lib.rs 95.23% <ø> (ø)
crates/onnx-genai-metadata/src/schema/mod.rs 81.39% <100.00%> (ø)
crates/onnx-genai-metadata/src/validation.rs 78.35% <ø> (+7.22%) ⬆️
crates/onnx-genai-comfyui-config/src/lib.rs 92.74% <50.00%> (+0.05%) ⬆️
...rates/onnx-genai-genai-config/src/compatibility.rs 60.00% <0.00%> (-0.12%) ⬇️
crates/onnx-genai-metadata/src/version.rs 99.05% <99.05%> (ø)
crates/onnx-genai-preprocess/src/image/program.rs 71.88% <0.00%> (ø)
crates/onnx-genai-metadata/src/schema/ir.rs 86.41% <75.00%> (-11.15%) ⬇️
... and 1 more

... and 66 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@justinchuby
justinchuby force-pushed the justinchuby/vision-batching-schema branch 4 times, most recently from 9d632d8 to d6df789 Compare August 24, 2026 21:42
justinchuby added a commit that referenced this pull request Aug 24, 2026
Two amendments adopted from the schema implementation (#2009), both places
where the implementation was stricter than this document and right to be.

**The packed axis rule now covers every packed value.** §4 rule 2 scoped
`axis == 0` to values a `batch_capacity` component consumes or produces. That
scope was too narrow: the runtime splits a packed value per request whenever it
hands one back — rule 8's emit path rebases offsets and derives per-request
owners with no capacity declaration in sight — and that split wants the same
contiguous span for the same aliasing reason. Under the old scope a strided
packed emit was legal precisely where nothing had declared it could pay for one.
No fixture in the tree declares a non-zero packed axis, so the wider rule
rejects nothing that exists.

**A preprocessing-program companion must carry the companion role.** §3.3
introduced `pack_offsets` and `pack_owner` but stated their use only
descriptively. §4 rule 4 now requires them of any level companion that is a
declared program output. The asymmetry with §3.2, where a length value is
resolved by name and its role is descriptive, is deliberate and now explained:
length vectors have established modality spellings that predate this design and
cannot be narrowed to one role, while the companion roles are new and have
nothing to accommodate — so requiring them costs nothing and catches a program
that wires, say, `grid_dimensions` into a `levels` entry at load rather than
shipping a plausible int64 rank-1 vector that means something else.

Decisions §10.5 and §10.6 restate both, matrix row 17 gains the two rejections,
and the P2 phase box's stale "§3.1 rules 1-7" is corrected to 1-9 to match the
body. No other rule, example, or acceptance row changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
…hat a document must say

Two contract-visible consequences of the schema implementation's code review
(#2009), recorded here so the design of record does not lag the contract.

**A ragged emit publishes the account of its raggedness.** §4 rule 8 required a
packed emit to declare every level's `offsets` and `owner`, but said nothing
about a padded emit, which is the same hole. §3.2 rejects a payload-shaped
validity mask precisely so the length vector is the only account of the padding
that exists anywhere; a caller that receives the payload without it holds
trailing entries it cannot distinguish from real ones. Rule 8 is generalized to
cover both, the serving carve-out now admits an `int64` rank-1 `shared` output
named as another emitted value's `valid_lengths` as well as one named as an
offsets/owner, and §3.2, decisions §10.5/§10.6, matrix row 25, and P2's negative
fixtures follow. A `valid_lengths` companion needs no rebasing on delivery — a
length is already relative to the item it measures — so a request receives the
slice indexing its own items.

**A version may gate what a document must say; never what it may say wrongly.**
§6.1 gains the distinction, prompted by a rule that demanded an explicit append
axis and thereby rejected two shipped diffusion packages that meant the default
they were written against. When a new field makes an existing default ambiguous,
the demand to state intent explicitly starts at the version that introduced the
ambiguity. Range checks and every rule about a value a document did state stay
unconditional at every version — relaxing those lets an older spelling assert
something false rather than merely leave something unsaid. The two look alike in
a validator and are opposites in effect: one preserves packages that predate a
question, the other would preserve their bugs. Packages under the exemption
should still be migrated, so it stays a guarantee rather than a live path
nothing exercises.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby force-pushed the justinchuby/vision-batching-schema branch 5 times, most recently from 2481092 to ae7e11e Compare August 24, 2026 23:56
justinchuby added a commit that referenced this pull request Aug 25, 2026
…ed is not published

Two corrections to §4 rule 8 and its mirrors in decisions §10.5/§10.6, both
raised by the schema PR (#2009) after a review found the second of them as a
live contradiction in its validator.

**The carve-out's "rank 1" was wrong for a length.** An `offsets` and an `owner`
are rank 1 by construction, so the flat phrasing was accurate for them and read
as though it were the general rule. It is not: §3.2 fixes a `valid_lengths` at
the number of axes outer to the padded one, so a value padded on axis 2 publishes
a rank-2 length vector. Under the flat reading, one rule of this design requires
that companion and another refuses it — and the refusal necessarily advises
`request_aligned`, which is exactly what a companion must not declare. Following
the narrower wording would make a correct package wrong.

The condition is therefore per *reference*: collect what each declaration naming
the value requires of it — role, and the rank that role implies at that site —
and admit the value if it satisfies any one. Three consequences are written down
because each is a place an implementer must choose. Expectations accumulate
rather than overwrite, since one length vector may legitimately bound the same
dimension of two outputs. Two declarations demanding different ranks of one value
is a genuine contradiction, but it belongs to the padding declaration that is
wrong; raising it at the serving rule names an innocent line. And where a padded
dimension does not resolve to an axis, the reference carries no rank expectation
at all — that declaration is already being rejected, and a second complaint about
a rank nothing could compute sends the reader somewhere there is nothing to fix.
A value named as a companion but shaped like something else is refused as a
malformed companion, naming role and expected rank, rather than falling through
to the generic message that advises the one layout a companion may not have.

**Declared is not published.** Both halves of rule 8 now require the companion to
be emitted — written by some declared step — not merely to appear in the output
list. An output nothing writes hands the caller an empty vector beside a ragged
payload, which is the precise failure the rule exists to prevent wearing the
appearance of compliance. The check is whole-workflow rather than path-sensitive:
"written by some declared step" is what is soundly decidable without evaluating
branch predicates, and a path-sensitive rule would reject a correct package whose
companion is written in a sibling branch of the emit.

Acceptance row 25 grows the cases that distinguish the two readings: doubly
padded values publishing rank-1 and rank-2 lengths together, a packed value
padded on the axes it does not pack publishing three companions at three ranks, a
mis-ranked length, and a companion declared but written by no step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby force-pushed the justinchuby/vision-batching-schema branch from ae7e11e to 56a27e8 Compare August 25, 2026 00:27
justinchuby added a commit that referenced this pull request Aug 25, 2026
…d say why

A follow-on to the previous commit, closing the asymmetry the schema PR (#2009)
found on its own side of the same rule.

The obligation requires a companion to be emitted; the carve-out already required
the value that *names* it to be emitted, but the neighbouring decidability
sentence still said "decidable from the declared outputs alone", which invites the
weaker reading. Asking one side for an emit while accepting a bare declaration on
the other is two answers to one question (Rule 10), and the weaker half is
exploitable: declare a padded output beside any `shared` vector, write neither,
and the vector walks past the serving rule on the strength of a payload that does
not exist. Both halves now say emitted, the exploit is written down as the reason,
and an expectation is stated to carry both the declaration that claims the value
and whether anything writes that declaration.

The decidability claim is corrected rather than dropped: the condition is decided
from the workflow's own declarations — its outputs and its steps — with no
runtime information. That is what made the check affordable, and it survives the
stronger reading; only "outputs alone" was wrong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…xis`

The rank clause was corrected two commits ago, but only in the form "the number
of axes outer to the padded one". That is the definition; it is not the check,
and an implementer has to derive one from the other. The schema PR (#2009) states
the same fact as `rank == axis` because that is what its validator evaluates, and
the derivation is exactly where the earlier flat "rank 1" survived review on both
sides.

Both spellings are now given together, in §3.2 where the shape rule lives, in §4
rule 8's carve-out, and in decisions §10.6. No rule changes; a reader of any of
the three gets the check without translating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI added 12 commits August 25, 2026 01:34
`token_packed` has been in the schema since the batching redesign, and
until now nothing checked it. A package could name an offsets value that
did not exist, an owner map that was a float matrix, or pack on an axis
its own rank did not have, and the validator would accept every one of
them — the layout was accepted for its shape, not its meaning. The two
values a packed layout names are precisely what lets a runtime split a
packed encoder result back into per-request pieces without any
serialized row identity, so a reference that resolves to nothing is a
contract no runtime can execute.

The other half of the same gap is that nothing in a package said how
many request rows one component invocation may carry. Absence has to
keep meaning "one row per invocation" — that is the only safe reading
for every component whose producer never thought about batching — so
the fact is added as an optional declaration that only ever widens what
a scheduler may do:

- `WorkflowComponent.batch_capacity` states the axis rows stack on, an
  optional `max_rows` bound, and the `uniform_axes` rows must already
  agree on before they can share a call. It denies unknown fields and
  defaults to absent, so every existing package keeps its meaning.
- `TensorContract.pad_mask` names the value whose booleans or integers
  mark which entries of a padded tensor are real. A padded batch is the
  only shape a fixed-arity component can consume, and recovering
  validity from a sentinel is exactly the hidden heuristic this schema
  refuses everywhere else.
- The image output content vocabulary gains `item_offsets` and
  `item_owner`, so a preprocessing program that packs the images of
  several requests can declare the two values that map items back to
  rows as content roles rather than by a name a runtime would guess.

`BatchLayout::TokenPacked` validation is now non-vacuous: offsets and
owner must resolve to a declared workflow value or to a port of the same
component, offsets must be a rank-one integer value that is
request-aligned on axis 0 (which is what its own schema doc has always
said it is), an owner map must be a rank-one integer value that is
either shared or packed against the same offsets, and the packed axis
must exist. A pad mask must resolve, be bool or integer, fit inside the
value it masks, and move with the same rows; a `token_packed` value has
no padding to mask at all. A declared capacity must agree with the ports
it batches, with any declared `row_scope`, and with a fixed row
dimension the graph already pins.

The one existing fixture that declared `token_packed` had `image_offsets`
as a shared `[batch_plus_one]` value, which the new check rejects; it
now declares the request-aligned per-row offsets the field is documented
to hold.

Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ideo

An encoder that batches clips faces the same questions an image encoder
does — how many rows fit in one invocation, which entries of a padded
tensor are real, and how packed entries map back to request rows — and
one more: those questions have to be answerable per item, per frame, and
per clip, not just per image.

The audit found no video surface at all. `preprocessing` declared an
image program and an audio program; a clip had nowhere to state its frame
sampling or its temporal padding, and the packed-batch content roles
stopped at `item_offsets`/`item_owner`. Meanwhile the still-image program
already carried `temporal_patch_size`, so the temporal axis was half
present under a name that denied it existed.

A frame is an image. The spatial transforms, their parameters, and their
output bindings are identical, so a second program type would have
duplicated forty fields to say the same thing twice. Instead the one
program becomes what it always was — a pixel program — and gains the two
operations a temporal axis adds:

  - `preprocessing.video` declares a `VisionPreprocessingProgram`, the
    same type `preprocessing.image` declares. Which key it appears under
    is what says whether a package preprocesses stills, clips, or both.
  - `sample_frames` and `pad_frames` join the transform vocabulary, with
    `fps`, `num_frames`, and `frame_stride` as their model data.
    `pad_frames` reuses `target_length`, the exact leading-axis length a
    pad produces.
  - Packing is layered, so each level names its own pair:
    `item_offsets`/`item_owner` for the patches an encoder consumes,
    `frame_offsets`/`frame_owner` for the frames a clip contributes, and
    `clip_offsets`/`clip_owner` when one request carries several clips.
    A still image uses the item pair alone.
  - `preprocessing.video` is validated exactly like the other two: one
    `onnx-genai.video-preprocess@1` adapter, invoked once, whose output
    ports agree with the program's declared contracts.

The batching contracts themselves did not change and did not need to.
`batch_capacity`, `pad_mask`, and `token_packed` never mentioned images,
so a clip padded on a temporal axis and a clip packed at frame
granularity are checked by the rules that already existed. What did carry
an image-specific name is renamed rather than aliased: `ImageTransform`,
`ImageOutputBinding`, `ImagePreprocessingProgram`, `ImageSizeSpec`, and
their two vocabularies become `Vision*`, and `ImageOutputValueRange`
becomes `PixelValueRange` — a video output role already read its
normalization from it. Serialized keys are unchanged, so every existing
package still parses.

Tests cover a temporally padded clip encoder end to end (sampling,
`pad_frames`, the frame mask its contract names, and a four-row
capacity), a clip packed at item and frame granularity at once, and the
rejections: a shared frame mask that cannot be permuted with its rows, a
mask out-ranking the clip it masks, a video program without its adapter,
and a frame owner map borrowed from the item packing.

Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A packed batch has more than one granularity once video is in scope. The
frames of a clip are packed, the clips of a request are packed, and the
frame owner map indexes clips rather than request rows. Until now
`token_packed` could only describe the terminal case, so a document either
flattened the structure away or left the second level to a convention no
reader could check.

`BatchLayout::TokenPacked` gains an optional `owner_span` naming the coarser
packing its owner indices point into. Absent is exactly what it was: owner
indices name request rows. Present shifts what the offsets are counted
against — one offset per span rather than one per row — and validation now
enforces that reading: the span must resolve and be packed itself, the
offsets must be packed the way that span's items are, and an owner map must
sit at the level of the items it indexes. Composition is bounded at frames
inside clips inside rows, with cycles rejected by name, because ownership is
walked on every compaction and a document that needs a third level is far
more likely to have looped.

`pad_mask` becomes a struct that names its axis. A rank-2 mask over a rank-5
clip could plausibly mark frames, tiles, or patches; guessing from rank is
the hidden heuristic this schema exists to refuse. The axis must be one the
value has, and never the axis its rows stack along: a compacted batch has no
padding rows. A request-scoped value's mask must span both the rows it is
permuted with and the axis it marks.

An incremental emit of a rank-4-or-deeper value must name its growth axis.
The default — the final axis — is right for a token sequence and silently
wrong for `[batch, frames, channels, height, width]`, where it is a spatial
extent. `uniform_axes` gains no new machinery: a temporal axis is an axis,
and the padded clip fixture now requires uniformity on frames and on both
spatial extents through one list.

The capacity error for a packed port now says where an invocation's rows
land instead of implying the port batches rows.

`WorkflowLoopIteration` is boxed in both loop forms. It carries a full
`TensorContract`, which the new fields grow past the point where clippy's
large_enum_variant fires on `WorkflowStep`; serde and the schema are
unaffected.

Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The architecture review of the design PR rejected the shape this branch
had encoded, and the revised design that answers it changed the spelling
of almost every contract this branch introduced.  Rather than ship a
schema that says the same things with different words, this restates the
whole surface in the design's own terms, so a package written against the
document validates against the code.

Three independent kinds of raggedness now sit on three dimensions of one
contract, and each is described by exactly one thing:

  * item count is an ownership level, `levels: [{ offsets, owner }]` on
    a single physical `token_packed` axis.  Nesting frames inside clips
    inside rows is a second level over the same axis, not a second axis;
    the validator checks the extent chain (level k's owner counts level
    k's units, level k's offsets counts its parent's plus one) instead of
    inventing a shape.  Depth is bounded at two.
  * item extent is `padding: [{ dimension, valid_lengths }]`.  The
    boolean mask this branch previously carried is withdrawn: padding is
    prefix-valid, so a length is O(items) where a mask is O(items x
    extent) and would force a device-to-host read to interpret.
  * item nesting composes the two without a third mechanism.

`batch_capacity` is keyed by shape symbol rather than axis index:
`uniform_dimensions` names the symbols that must agree for a batch to
form, and `budgets: [{ dimensions, max_total }]` bounds the product of a
symbol set.  There is no `max_items` integer, because the thing being
limited is always some dimension the contracts already named.  Symbols
that no port declares, or that resolve ambiguously, are rejected.

`packed_extent: preserved | produced` says where a packed output's level
zero companions came from: preserved reuses what already described the
extent, produced is described by companions the graph emits.  A packed
emit publishes the companions of every level, because a caller can only
split a packed result with the companions that describe it.

The preprocessing vocabulary gains two level-agnostic roles,
`pack_offsets` and `pack_owner`, and generalises `valid_lengths` from the
audio side.  It contains no clip, no frame, and no window: a video
packing is the same two roles applied twice.

Row scope is checked against request axes only, and never against a
port's packed axis; the packed item axis is not a row axis.  Load-time
validation reads graphs and contracts alone -- offsets monotonicity,
owner range and budget satisfaction are invocation-time value invariants
and stay there.  Packed axis zero is required, so a split is a slice.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The second architecture review of the encoder-batching design found three
things this schema got wrong. Each is a case of one declaration answering a
question that has more than one answer.

A packed value's ownership chain carried a single `packed_extent`, so a
component had to say that it produced every level or preserved every level.
The canonical video encoder does neither: it merges frames into a chosen
number of tokens per clip while leaving the clip-to-request grouping exactly
as it received it. `extent` therefore moves onto `OwnershipLevel`, where the
question is actually asked. A `produced` level must name companions the
component itself outputs. A `preserved` level must not, and must correspond to
some input level by its `(offsets, owner)` pair -- by identity, because an
encoder that pools an inner level away leaves the outer one at a different
index, and matching by position would silently accept the wrong chain.

Every structure in this schema denies unknown fields, so a newer document is
not one an older runtime ignores: it is one that runtime rejects, field by
field, with `unknown field 'batch_capacity'`. That is the least actionable
order in which to learn that a package needs a newer build. `version.rs` adds
the grammar the ecosystem already speaks -- absent, `v1`, `1`, `1.0` and
`v1.0` all normalize to 1.0, so no document in the tree changes meaning -- and
a gate that reads the version from the untyped tree and compares it before
`serde` sees the bytes. `parse_metadata`, `parse_metadata_json` and
`load_metadata` are now the only ways a document becomes typed metadata, and
the comfyui lowering, the model.io migration binary and the comfyui config
loader go through them. A document that uses a 1.1 field while declaring 1.0
is refused by validation for that reason, in those words.

`uniform_dimensions` could name any shape symbol, including the packed extent
itself and an ownership level's unit count. Pinning one of those says every
group has the same number of items, which is a footprint bound wearing the
wrong hat -- `budgets` already says that, and says it in a form that composes.
Uniform symbols are now ordinary per-item dimensions only; a redundant
singleton budget on a pinned symbol is refused, while a composed budget that
multiplies a pinned extent by a count remains the way footprint is bounded.
A fixed-frame video is expressed as a pinned temporal dimension with no frame
ownership level at all.

Also: `valid_lengths` is a new generic content role, and the audio roles that
already mean a length keep meaning it; ownership and length companions must
carry a role that says which they are; and a declared `owner` map is runtime
internal, so a workflow input naming one may not be `externally_suppliable`.
Offsets stay suppliable, because a caller can state its own and the runtime
rebases them.

The comfyui golden document now stamps the canonical `1.0` rather than `v1`.
Readers accept both, and the emitted spelling should be the one the grammar
calls canonical.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sion

Corrections from coordinating against the revised design of record
(justinchuby/vision-batching-design 2daeb83). Each is a case of a rule that
was right about the wrong quantity.

`budgets` rejected a singleton entry only when it named a *pinned* symbol.
The property that makes a bound meaningless is not pinning, it is failing to
reach the group: `patches` alone counts patches per frame, which is the same
number whether the invocation holds one frame or a thousand, and it is equally
meaningless whether `patches` is pinned or padded. A `dimensions` list is now
read as a nesting path, outermost first, and its first symbol must be a count
the scheduler chose -- a packed extent, an ownership level's unit count, or the
row axis of a component that pads rather than packs. Inner per-item symbols
appear only in later positions, where they multiply a count into a footprint.

A fixed frame count drops the frames-to-clips level but keeps the clips-to-rows
level, which the previous revision got wrong by asserting a fixed-frame video
declared no ownership at all. A request still contributes whatever number of
clips it carries, and a request-aligned tensor would have to pad that away --
the cost packing exists to avoid. `FIXED_FRAME_VIDEO_ENCODER` states the shape
the design gives: `[clips, frames, patches, features]` packed over clips, with
`frames` an ordinary pinned dimension.

`SCHEMA_VERSION` goes back to `v1`. Normalizing on read is what makes the four
spellings in the wild interchangeable; changing what writers stamp would rewrite
bytes and semantic identities to say something no reader distinguishes. The
comfyui golden document is byte-identical to main again. `v1.1` is the canonical
spelling a batching document declares, `1.1` is an accepted synonym, and
`SchemaVersion` displays with the `v` so errors name a version the way a document
would write it.

A `padding` entry names its length value by name, so nothing dispatches on the
value's content role. An audio program points its entry at the `frame_lengths`
it already emits rather than emitting a second copy under the generic name, and
`LENGTH_CONTENT_ROLES` becomes informative -- `valid_frames` and `valid_samples`
leave it, since those are counts a graph consumes rather than statements about
padding.

Two rules gain the halves they were missing. A companion pair is one mapping
wherever it is named, so every port naming it must resolve both of its extent
symbols the same way -- not merely pair the same two names. And the serving
carve-out is narrow by construction: a `shared` emitted value is admitted only
when it is `int64`, rank 1, and named as an offsets or owner of some other
emitted value's layout.

One deliberate strengthening, recorded rather than assumed: the packed axis must
be 0 for every packed value, not only for one a `batch_capacity` component
touches. The reason the design gives -- a no-copy view is a contiguous element
window -- applies whenever a runtime splits a packed result per request, which
includes a packed emit with no capacity in sight.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three defects from review, all of them a rule reaching further than its reason.

**A shipped package did not expire.** The rank-4 append rule refused the
checked-in diffusion and guided-diffusion packages, which append a latent
`[batch, 4, height, width]` along its final axis. They are correct and always
were: the final axis is what the default meant, and they had no way to say so
explicitly because nothing asked. The rule exists because a rank-4 *media*
tensor makes that axis ambiguous, and a rank-4 media tensor is what v1.1
introduced -- so the demand starts there. An older document keeps the default it
was written against; a document declaring v1.1 names the axis. An axis that is
outside the value's rank names nothing at any version and is still rejected.

Version scoping means the validator has to be told the version, so
`validate_pipeline_spec` now takes one. Deriving it from the document's fields
would have been the same fact stored twice, and defaulting it inside the
validator would have made the strictness depend on which entry point a caller
happened to use. Both fixtures are migrated to `axis: 3` regardless: stating the
axis the default already chose costs nothing and outlives the exemption.

**A padded output could hide the only account of its padding.** The serving rule
that every emitted rank>0 value declare a row correspondence admitted ownership
offsets and owner maps, because those are `shared` by construction. A padded
output's `valid_lengths` is `shared` for the same reason -- one entry per
position outside the dimension it bounds -- and was rejected. The carve-out now
names both, and stays decidable from the declared outputs alone: an unrelated
`shared` result is still a per-request answer with no way back to a request.

The other half of that symmetry was missing too. `validate_packed_emit_companions`
requires a packed emit to publish the companions that split it; nothing required
a padded emit to publish the lengths that read it. A caller holding trailing
entries that mean nothing, with no way to learn how many, is the failure the
padding contract exists to prevent -- and the schema deliberately refuses a
payload-shaped validity mask as the alternative, so that vector is the only
account there is.

**A user-facing error was malformed.** The unquoted-`schema_version` message was
missing its line continuations, so it reached a reader with fourteen-space runs
in the middle of two sentences.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An alignment check against the design of record found the published field
documentation still describing the canonical spelling as `<major>.<minor>` with
an example of `"1.1"`. That is not what this crate does. Every writer in the
repository stamps `v1`, and a document that uses a batching field is told to
declare `v1.1`; a schema whose own description drops the `v` leaves one format
with two house styles and no way to tell which a writer should follow.

The field now states the canonical form is `v<major>.<minor>`, lists the
spellings that predate it, names `1.1` as an accepted synonym for `v1.1` rather
than an equal alternative, and says plainly that a document using nothing new
keeps stamping what it already stamped.

Three tests pin what the prose now claims, because a house style that only
exists in a doc comment is a suggestion:

- the canonical spellings are exactly `v1` and `v1.1`, and `v1.1` is the literal
  a batching document is *told* to write, not merely one the reader tolerates;
- `1.1` normalizes to the same version and is not that literal;
- every spelling of the first version survives a load verbatim and keeps its own
  semantic identity, so normalizing for comparison never becomes normalizing on
  disk.

No document bytes change. The two diffusion fixtures this branch touches carry
the same `schema_version` they always did; the comfyui golden is byte-identical
to main.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both extensions this branch went past the design with are now in the design of
record (`justinchuby/vision-batching-design` 7b20e31), so they stop being
divergences and start being rules — which means they need tests that say what
they are, not tests that happen to cover them.

The packed axis must be 0 wherever `token_packed` appears. The design had
scoped that to a component declaring `batch_capacity`; the scope was the bug,
because the emit path rebases offsets and derives per-request owners with no
capacity in sight and wants exactly the same contiguity. The validator already
enforced it unconditionally. A test now states the unconditional form, on a
workflow that declares no capacity at all, so a future reader cannot mistake the
enforcement for an oversight.

The companion-role requirement reaches only declarations that have a role. A
preprocessing program declares what each output *is*, so a program wiring some
other plausible int64 rank-1 vector into a `levels` entry is caught at load; a
companion the graph produces is an ONNX output port, and a port contract has
nowhere to carry a content role. The validator was already scoped this way --
the role lookup simply finds nothing for a value that is not a declared program
output -- but nothing said so, and a `produced` level being quietly exempt is
exactly the kind of behaviour that gets "fixed" into a bug. A test now asserts
the exemption on a fixture whose companions are graph outputs.

The comment on the role rule also records why `padding` deliberately does not
get the same treatment, so the two are not read as an inconsistency: length
vectors have established modality spellings that programs legitimately emit, so
no single role could be demanded without breaking a program that predates the
generic name, while the two ownership roles are new and reject nothing that
exists.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The three ragged-emit companions do not behave alike on delivery, and the
difference was implemented but never asserted. An owner map is
invocation-global and is never handed to a request; offsets are per-request
but group-relative, so they are delivered rebased to the request's own
start; a valid_lengths is already relative to the item it measures and
means the same number in any group, so a request states its own and
receives the slice indexing its own items.

Only the owner rule constrains suppliability, so the third case held by
construction. Assert it, so a later attempt to unify the two delivered
companions with the internal one fails a test rather than passing silently,
and say in the owner rule's comment why it reaches the owner and nothing
else.

Also quote the general form of the version split in validate_emit_axis: a
version may gate what a document must say, but never what a document may
say wrongly. The specific rule already worked this way; the principle is
what a reader needs to apply it to the next additive field.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ion it needs

`token_packed` used to carry `offsets` and `owner` directly on the batch
layout, which could say only that items sat in request rows. It now carries
`levels`, an ownership chain, because an item can itself be a group --
frames in clips in rows -- and one pair cannot say that.

A one-level chain is exactly what the flat pair meant, so a shim would be
expressible. It is refused rather than translated: this schema is
pre-release and rule 3 is to reshape completely rather than keep a second
spelling alive, and no in-tree package uses `token_packed`, so nothing
ships that a translation would rescue.

What a clean break costs is the error, and that cost was unpaid. `serde`
reported the old shape as "unknown field `offsets`", which reads like a
typo -- a reader goes looking for a misspelling rather than for a
migration, and the reported path stopped at the contract rather than the
layout. `reject_flat_token_packed` recognizes the retired shape in the
preparse, names the containing path, prints the replacement, and says
plainly that nothing will convert the document for it.

It runs after the version gate, deliberately. The flat pair is a reshape
inside the v1 line, so refusing it presumes the document belongs to that
line; a document declaring a version this build cannot read is refused for
that instead, since a reader that has never seen v2 has no standing to say
what a spelling means there.

The compatibility claims are corrected to match. Absence-as-compatibility
is a statement about fields that were *added*; it was written as though it
covered everything, and a document using nothing new now fails to load. The
`schema_version` doc and the version test module both say so and point at
the refusal. The published schema already required `levels` and admitted no
extras, so schema and loader agree; a test pins that agreement rather than
leaving it to hold by accident.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rank one

The serving carve-out asked whether an emitted `shared` value was named as
some declared output's companion and whether it was int64 rank 1. That is
right for an ownership offsets or owner map, which are rank 1 by
construction, and wrong for a validity length, whose rank is the axis index
of the dimension it bounds: one entry per position of the axes outer to it.
A rank-2 length for a padded axis 2 is exactly correct and was refused as
"a per-request value without a declared batch_layout" -- advice that, if
followed, would have made it wrong.

The bare name set becomes a map from companion name to what the
declarations naming it require: the role, and the rank that role implies at
that site. A name can be claimed by several declarations, so expectations
are collected rather than overwritten and a value satisfies the carve-out
by matching any one of them; two declarations demanding different ranks of
one value is a contradiction for validate_padding to report against the
declaration that is wrong, not for this rule to report as a missing row
axis. Where a padded dimension does not resolve to an axis the expectation
carries no rank, because a second complaint about a rank nothing could
compute would send a reader to the wrong line.

A value named as a companion but shaped like something else now gets that
as its error. The generic message told its author to declare a row axis,
which is precisely what a companion must not do.

Both companion obligations also now require the companion to be emitted,
not merely declared. Declaring an output and writing it are different
facts, and only the second delivers anything: a workflow that declares its
lengths and never emits them satisfies "publishes them" while handing the
caller a padded tensor and an empty vector, which is the failure the
obligation exists to prevent. The check is whole-workflow rather than
path-sensitive, deliberately -- whether two emits reach a caller together
depends on a branch predicate, and this crate does not evaluate predicates,
so a path-sensitive rule would reject correct packages whose companion is
written in a sibling branch. "Somewhere in the declared steps" is what can
be decided soundly, and it catches the case that occurs.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…hat a document must say

Two contract-visible consequences of the schema implementation's code review
(#2009), recorded here so the design of record does not lag the contract.

**A ragged emit publishes the account of its raggedness.** §4 rule 8 required a
packed emit to declare every level's `offsets` and `owner`, but said nothing
about a padded emit, which is the same hole. §3.2 rejects a payload-shaped
validity mask precisely so the length vector is the only account of the padding
that exists anywhere; a caller that receives the payload without it holds
trailing entries it cannot distinguish from real ones. Rule 8 is generalized to
cover both, the serving carve-out now admits an `int64` rank-1 `shared` output
named as another emitted value's `valid_lengths` as well as one named as an
offsets/owner, and §3.2, decisions §10.5/§10.6, matrix row 25, and P2's negative
fixtures follow. A `valid_lengths` companion needs no rebasing on delivery — a
length is already relative to the item it measures — so a request receives the
slice indexing its own items.

**A version may gate what a document must say; never what it may say wrongly.**
§6.1 gains the distinction, prompted by a rule that demanded an explicit append
axis and thereby rejected two shipped diffusion packages that meant the default
they were written against. When a new field makes an existing default ambiguous,
the demand to state intent explicitly starts at the version that introduced the
ambiguity. Range checks and every rule about a value a document did state stay
unconditional at every version — relaxing those lets an older spelling assert
something false rather than merely leave something unsaid. The two look alike in
a validator and are opposites in effect: one preserves packages that predate a
question, the other would preserve their bugs. Packages under the exemption
should still be migrated, so it stays a guarantee rather than a live path
nothing exercises.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…ed is not published

Two corrections to §4 rule 8 and its mirrors in decisions §10.5/§10.6, both
raised by the schema PR (#2009) after a review found the second of them as a
live contradiction in its validator.

**The carve-out's "rank 1" was wrong for a length.** An `offsets` and an `owner`
are rank 1 by construction, so the flat phrasing was accurate for them and read
as though it were the general rule. It is not: §3.2 fixes a `valid_lengths` at
the number of axes outer to the padded one, so a value padded on axis 2 publishes
a rank-2 length vector. Under the flat reading, one rule of this design requires
that companion and another refuses it — and the refusal necessarily advises
`request_aligned`, which is exactly what a companion must not declare. Following
the narrower wording would make a correct package wrong.

The condition is therefore per *reference*: collect what each declaration naming
the value requires of it — role, and the rank that role implies at that site —
and admit the value if it satisfies any one. Three consequences are written down
because each is a place an implementer must choose. Expectations accumulate
rather than overwrite, since one length vector may legitimately bound the same
dimension of two outputs. Two declarations demanding different ranks of one value
is a genuine contradiction, but it belongs to the padding declaration that is
wrong; raising it at the serving rule names an innocent line. And where a padded
dimension does not resolve to an axis, the reference carries no rank expectation
at all — that declaration is already being rejected, and a second complaint about
a rank nothing could compute sends the reader somewhere there is nothing to fix.
A value named as a companion but shaped like something else is refused as a
malformed companion, naming role and expected rank, rather than falling through
to the generic message that advises the one layout a companion may not have.

**Declared is not published.** Both halves of rule 8 now require the companion to
be emitted — written by some declared step — not merely to appear in the output
list. An output nothing writes hands the caller an empty vector beside a ragged
payload, which is the precise failure the rule exists to prevent wearing the
appearance of compliance. The check is whole-workflow rather than path-sensitive:
"written by some declared step" is what is soundly decidable without evaluating
branch predicates, and a path-sensitive rule would reject a correct package whose
companion is written in a sibling branch of the emit.

Acceptance row 25 grows the cases that distinguish the two readings: doubly
padded values publishing rank-1 and rank-2 lengths together, a packed value
padded on the axes it does not pack publishing three companions at three ranks, a
mis-ranked length, and a companion declared but written by no step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…d say why

A follow-on to the previous commit, closing the asymmetry the schema PR (#2009)
found on its own side of the same rule.

The obligation requires a companion to be emitted; the carve-out already required
the value that *names* it to be emitted, but the neighbouring decidability
sentence still said "decidable from the declared outputs alone", which invites the
weaker reading. Asking one side for an emit while accepting a bare declaration on
the other is two answers to one question (Rule 10), and the weaker half is
exploitable: declare a padded output beside any `shared` vector, write neither,
and the vector walks past the serving rule on the strength of a payload that does
not exist. Both halves now say emitted, the exploit is written down as the reason,
and an expectation is stated to carry both the declaration that claims the value
and whether anything writes that declaration.

The decidability claim is corrected rather than dropped: the condition is decided
from the workflow's own declarations — its outputs and its steps — with no
runtime information. That is what made the check affordable, and it survives the
stronger reading; only "outputs alone" was wrong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…xis`

The rank clause was corrected two commits ago, but only in the form "the number
of axes outer to the padded one". That is the definition; it is not the check,
and an implementer has to derive one from the other. The schema PR (#2009) states
the same fact as `rank == axis` because that is what its validator evaluates, and
the derivation is exactly where the earlier flat "rank 1" survived review on both
sides.

Both spellings are now given together, in §3.2 where the shape rule lives, in §4
rule 8's carve-out, and in decisions §10.6. No rule changes; a reader of any of
the three gets the check without translating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
PR #2009 merged as 0448f2b, which lands phases P1 and P2 of this design:
`WorkflowComponent.batch_capacity`, `TensorContract.padding`, the `levels`
ownership chain, the `pack_offsets` / `pack_owner` / `valid_lengths` content
roles, the `schema_version` gate, and the load-time validators for all of
them. The design of record was still written as if none of it existed, which
made its status line, its evidence section, and 57 of its source citations
false.

What changed:

- Status is `partly implemented` rather than `proposed`, naming which phases
  landed and which did not. The interpreter, preprocessor, scheduler, and
  backend phases remain unimplemented, so a package can now *declare* a
  batched encoder that no runtime will group; §6 already requires that
  asymmetry to be safe, and the note says so.
- §2 keeps its observations in the past tense and adds a `Since #2009` note
  per subsection. The observations are the record of why each field exists
  and are not recoverable from the field, so they are marked rather than
  deleted. §2.3, §2.4, and §2.5 are untouched because nothing closed them.
- §3.3 and the `valid_lengths` argument no longer claim the roles are absent
  from the vocabulary; they are declarable and nothing produces them, which
  is a different and still-open gap.
- §6.1's `schema_version` doc-comment defect is recorded as corrected, in the
  narrow form this design argued for: compatibility comes from a field being
  absent by default, which buys nothing for a field that was reshaped.
- §8 marks P1 and P2 as shipped while keeping the full obligation text, so
  the phase list stays reviewable against the design instead of decaying into
  a changelog.
- INFERENCE_METADATA_DECISIONS.md §10.4 becomes `What §10.1 did not have`,
  and §10.5 / §10.6 drop `proposed` for a status note that separates the
  load-time half now in force from the runtime half that is not. DESIGN.md
  and docs/README.md follow.
- 57 citations re-pinned across the two documents, 39 mechanically and 16 by
  hand where the merge moved the claim rather than the line. Several were
  in range but semantically wrong after the `Image*` to `Vision*` rename,
  which line-number repair alone would not have caught.

Two pre-existing defects found while checking: a link to a renamed §6 anchor,
and a short-form `redesign_invariants.rs:158` citation appearing before its
first full path. Both fixed.

Docs only. Verified: every intra-doc link and anchor resolves, every
citation is in range and reads as claimed, 28 acceptance rows with uniform
columns, `git diff --check` clean, lines within 88 columns.
`cargo test -p onnx-genai-metadata --test encoder_batching` passes 104 tests
at this main, which is the evidence for the P2 marker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…fy by content

#2009 merged as `0448f2bc6`, moving the files this document cites most. The
citations were re-pinned in an earlier commit on this branch, but the check
that guarded them asked only whether a line number fell inside the file. That
is a property of the file's *length*, not of its *content*: a citation can
point at unrelated code that reads plausibly and still pass, and the failure is
silent. Verification is now content-aware — for each of the 97 ranged
citations, the range's **first** line must contain the symbol or comment the
surrounding sentence claims, and the range must contain a second distinctive
substring. Anchoring on the head line makes the check position-sensitive, so
every citation fails under a one-line drift rather than passing.

Applying it to the merged tree found three references that were in range and
wrong at the head, all of them narrowings of a previously correct range:

- `schema/package.rs:158-174` -> `162-174`. The claim is about
  `batch_invariance`; lines 158-161 are the unrelated `decoding` field, so the
  citation opened on someone else's declaration.
- `ort/value.rs:1543-1551` -> `1544-1551`. The claim names
  `alias_with_offset`; 1543 is the closing line of the preceding doc comment.
- `ort/value.rs:1526-1542` -> `1524-1543`, in both documents. The claim quotes
  the no-copy-view doc comment; 1526 is a blank `///`. Re-pinned to the whole
  comment block, since a block citation cannot be off by one.

The remaining 54 ranges verify unchanged against the merged tree, including all
80 citations into the 24 files #2009 touched.

Records the convention in the header so the next editor re-pins by locating the
named symbol rather than by adjusting the number until it lands inside the file.

Docs only: no code, schema, or fixture changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
Two amendments adopted from the schema implementation (#2009), both places
where the implementation was stricter than this document and right to be.

**The packed axis rule now covers every packed value.** §4 rule 2 scoped
`axis == 0` to values a `batch_capacity` component consumes or produces. That
scope was too narrow: the runtime splits a packed value per request whenever it
hands one back — rule 8's emit path rebases offsets and derives per-request
owners with no capacity declaration in sight — and that split wants the same
contiguous span for the same aliasing reason. Under the old scope a strided
packed emit was legal precisely where nothing had declared it could pay for one.
No fixture in the tree declares a non-zero packed axis, so the wider rule
rejects nothing that exists.

**A preprocessing-program companion must carry the companion role.** §3.3
introduced `pack_offsets` and `pack_owner` but stated their use only
descriptively. §4 rule 4 now requires them of any level companion that is a
declared program output. The asymmetry with §3.2, where a length value is
resolved by name and its role is descriptive, is deliberate and now explained:
length vectors have established modality spellings that predate this design and
cannot be narrowed to one role, while the companion roles are new and have
nothing to accommodate — so requiring them costs nothing and catches a program
that wires, say, `grid_dimensions` into a `levels` entry at load rather than
shipping a plausible int64 rank-1 vector that means something else.

Decisions §10.5 and §10.6 restate both, matrix row 17 gains the two rejections,
and the P2 phase box's stale "§3.1 rules 1-7" is corrected to 1-9 to match the
body. No other rule, example, or acceptance row changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…hat a document must say

Two contract-visible consequences of the schema implementation's code review
(#2009), recorded here so the design of record does not lag the contract.

**A ragged emit publishes the account of its raggedness.** §4 rule 8 required a
packed emit to declare every level's `offsets` and `owner`, but said nothing
about a padded emit, which is the same hole. §3.2 rejects a payload-shaped
validity mask precisely so the length vector is the only account of the padding
that exists anywhere; a caller that receives the payload without it holds
trailing entries it cannot distinguish from real ones. Rule 8 is generalized to
cover both, the serving carve-out now admits an `int64` rank-1 `shared` output
named as another emitted value's `valid_lengths` as well as one named as an
offsets/owner, and §3.2, decisions §10.5/§10.6, matrix row 25, and P2's negative
fixtures follow. A `valid_lengths` companion needs no rebasing on delivery — a
length is already relative to the item it measures — so a request receives the
slice indexing its own items.

**A version may gate what a document must say; never what it may say wrongly.**
§6.1 gains the distinction, prompted by a rule that demanded an explicit append
axis and thereby rejected two shipped diffusion packages that meant the default
they were written against. When a new field makes an existing default ambiguous,
the demand to state intent explicitly starts at the version that introduced the
ambiguity. Range checks and every rule about a value a document did state stay
unconditional at every version — relaxing those lets an older spelling assert
something false rather than merely leave something unsaid. The two look alike in
a validator and are opposites in effect: one preserves packages that predate a
question, the other would preserve their bugs. Packages under the exemption
should still be migrated, so it stays a guarantee rather than a live path
nothing exercises.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…ed is not published

Two corrections to §4 rule 8 and its mirrors in decisions §10.5/§10.6, both
raised by the schema PR (#2009) after a review found the second of them as a
live contradiction in its validator.

**The carve-out's "rank 1" was wrong for a length.** An `offsets` and an `owner`
are rank 1 by construction, so the flat phrasing was accurate for them and read
as though it were the general rule. It is not: §3.2 fixes a `valid_lengths` at
the number of axes outer to the padded one, so a value padded on axis 2 publishes
a rank-2 length vector. Under the flat reading, one rule of this design requires
that companion and another refuses it — and the refusal necessarily advises
`request_aligned`, which is exactly what a companion must not declare. Following
the narrower wording would make a correct package wrong.

The condition is therefore per *reference*: collect what each declaration naming
the value requires of it — role, and the rank that role implies at that site —
and admit the value if it satisfies any one. Three consequences are written down
because each is a place an implementer must choose. Expectations accumulate
rather than overwrite, since one length vector may legitimately bound the same
dimension of two outputs. Two declarations demanding different ranks of one value
is a genuine contradiction, but it belongs to the padding declaration that is
wrong; raising it at the serving rule names an innocent line. And where a padded
dimension does not resolve to an axis, the reference carries no rank expectation
at all — that declaration is already being rejected, and a second complaint about
a rank nothing could compute sends the reader somewhere there is nothing to fix.
A value named as a companion but shaped like something else is refused as a
malformed companion, naming role and expected rank, rather than falling through
to the generic message that advises the one layout a companion may not have.

**Declared is not published.** Both halves of rule 8 now require the companion to
be emitted — written by some declared step — not merely to appear in the output
list. An output nothing writes hands the caller an empty vector beside a ragged
payload, which is the precise failure the rule exists to prevent wearing the
appearance of compliance. The check is whole-workflow rather than path-sensitive:
"written by some declared step" is what is soundly decidable without evaluating
branch predicates, and a path-sensitive rule would reject a correct package whose
companion is written in a sibling branch of the emit.

Acceptance row 25 grows the cases that distinguish the two readings: doubly
padded values publishing rank-1 and rank-2 lengths together, a packed value
padded on the axes it does not pack publishing three companions at three ranks, a
mis-ranked length, and a companion declared but written by no step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…d say why

A follow-on to the previous commit, closing the asymmetry the schema PR (#2009)
found on its own side of the same rule.

The obligation requires a companion to be emitted; the carve-out already required
the value that *names* it to be emitted, but the neighbouring decidability
sentence still said "decidable from the declared outputs alone", which invites the
weaker reading. Asking one side for an emit while accepting a bare declaration on
the other is two answers to one question (Rule 10), and the weaker half is
exploitable: declare a padded output beside any `shared` vector, write neither,
and the vector walks past the serving rule on the strength of a payload that does
not exist. Both halves now say emitted, the exploit is written down as the reason,
and an expectation is stated to carry both the declaration that claims the value
and whether anything writes that declaration.

The decidability claim is corrected rather than dropped: the condition is decided
from the workflow's own declarations — its outputs and its steps — with no
runtime information. That is what made the check affordable, and it survives the
stronger reading; only "outputs alone" was wrong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…xis`

The rank clause was corrected two commits ago, but only in the form "the number
of axes outer to the padded one". That is the definition; it is not the check,
and an implementer has to derive one from the other. The schema PR (#2009) states
the same fact as `rank == axis` because that is what its validator evaluates, and
the derivation is exactly where the earlier flat "rank 1" survived review on both
sides.

Both spellings are now given together, in §3.2 where the shape rule lives, in §4
rule 8's carve-out, and in decisions §10.6. No rule changes; a reader of any of
the three gets the check without translating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
PR #2009 merged as 0448f2b, which lands phases P1 and P2 of this design:
`WorkflowComponent.batch_capacity`, `TensorContract.padding`, the `levels`
ownership chain, the `pack_offsets` / `pack_owner` / `valid_lengths` content
roles, the `schema_version` gate, and the load-time validators for all of
them. The design of record was still written as if none of it existed, which
made its status line, its evidence section, and 57 of its source citations
false.

What changed:

- Status is `partly implemented` rather than `proposed`, naming which phases
  landed and which did not. The interpreter, preprocessor, scheduler, and
  backend phases remain unimplemented, so a package can now *declare* a
  batched encoder that no runtime will group; §6 already requires that
  asymmetry to be safe, and the note says so.
- §2 keeps its observations in the past tense and adds a `Since #2009` note
  per subsection. The observations are the record of why each field exists
  and are not recoverable from the field, so they are marked rather than
  deleted. §2.3, §2.4, and §2.5 are untouched because nothing closed them.
- §3.3 and the `valid_lengths` argument no longer claim the roles are absent
  from the vocabulary; they are declarable and nothing produces them, which
  is a different and still-open gap.
- §6.1's `schema_version` doc-comment defect is recorded as corrected, in the
  narrow form this design argued for: compatibility comes from a field being
  absent by default, which buys nothing for a field that was reshaped.
- §8 marks P1 and P2 as shipped while keeping the full obligation text, so
  the phase list stays reviewable against the design instead of decaying into
  a changelog.
- INFERENCE_METADATA_DECISIONS.md §10.4 becomes `What §10.1 did not have`,
  and §10.5 / §10.6 drop `proposed` for a status note that separates the
  load-time half now in force from the runtime half that is not. DESIGN.md
  and docs/README.md follow.
- 57 citations re-pinned across the two documents, 39 mechanically and 16 by
  hand where the merge moved the claim rather than the line. Several were
  in range but semantically wrong after the `Image*` to `Vision*` rename,
  which line-number repair alone would not have caught.

Two pre-existing defects found while checking: a link to a renamed §6 anchor,
and a short-form `redesign_invariants.rs:158` citation appearing before its
first full path. Both fixed.

Docs only. Verified: every intra-doc link and anchor resolves, every
citation is in range and reads as claimed, 28 acceptance rows with uniform
columns, `git diff --check` clean, lines within 88 columns.
`cargo test -p onnx-genai-metadata --test encoder_batching` passes 104 tests
at this main, which is the evidence for the P2 marker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…fy by content

#2009 merged as `0448f2bc6`, moving the files this document cites most. The
citations were re-pinned in an earlier commit on this branch, but the check
that guarded them asked only whether a line number fell inside the file. That
is a property of the file's *length*, not of its *content*: a citation can
point at unrelated code that reads plausibly and still pass, and the failure is
silent. Verification is now content-aware — for each of the 97 ranged
citations, the range's **first** line must contain the symbol or comment the
surrounding sentence claims, and the range must contain a second distinctive
substring. Anchoring on the head line makes the check position-sensitive, so
every citation fails under a one-line drift rather than passing.

Applying it to the merged tree found three references that were in range and
wrong at the head, all of them narrowings of a previously correct range:

- `schema/package.rs:158-174` -> `162-174`. The claim is about
  `batch_invariance`; lines 158-161 are the unrelated `decoding` field, so the
  citation opened on someone else's declaration.
- `ort/value.rs:1543-1551` -> `1544-1551`. The claim names
  `alias_with_offset`; 1543 is the closing line of the preceding doc comment.
- `ort/value.rs:1526-1542` -> `1524-1543`, in both documents. The claim quotes
  the no-copy-view doc comment; 1526 is a blank `///`. Re-pinned to the whole
  comment block, since a block citation cannot be off by one.

The remaining 54 ranges verify unchanged against the merged tree, including all
80 citations into the 24 files #2009 touched.

Records the convention in the header so the next editor re-pins by locating the
named symbol rather than by adjusting the number until it lands inside the file.

Docs only: no code, schema, or fixture changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
Two amendments adopted from the schema implementation (#2009), both places
where the implementation was stricter than this document and right to be.

**The packed axis rule now covers every packed value.** §4 rule 2 scoped
`axis == 0` to values a `batch_capacity` component consumes or produces. That
scope was too narrow: the runtime splits a packed value per request whenever it
hands one back — rule 8's emit path rebases offsets and derives per-request
owners with no capacity declaration in sight — and that split wants the same
contiguous span for the same aliasing reason. Under the old scope a strided
packed emit was legal precisely where nothing had declared it could pay for one.
No fixture in the tree declares a non-zero packed axis, so the wider rule
rejects nothing that exists.

**A preprocessing-program companion must carry the companion role.** §3.3
introduced `pack_offsets` and `pack_owner` but stated their use only
descriptively. §4 rule 4 now requires them of any level companion that is a
declared program output. The asymmetry with §3.2, where a length value is
resolved by name and its role is descriptive, is deliberate and now explained:
length vectors have established modality spellings that predate this design and
cannot be narrowed to one role, while the companion roles are new and have
nothing to accommodate — so requiring them costs nothing and catches a program
that wires, say, `grid_dimensions` into a `levels` entry at load rather than
shipping a plausible int64 rank-1 vector that means something else.

Decisions §10.5 and §10.6 restate both, matrix row 17 gains the two rejections,
and the P2 phase box's stale "§3.1 rules 1-7" is corrected to 1-9 to match the
body. No other rule, example, or acceptance row changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…hat a document must say

Two contract-visible consequences of the schema implementation's code review
(#2009), recorded here so the design of record does not lag the contract.

**A ragged emit publishes the account of its raggedness.** §4 rule 8 required a
packed emit to declare every level's `offsets` and `owner`, but said nothing
about a padded emit, which is the same hole. §3.2 rejects a payload-shaped
validity mask precisely so the length vector is the only account of the padding
that exists anywhere; a caller that receives the payload without it holds
trailing entries it cannot distinguish from real ones. Rule 8 is generalized to
cover both, the serving carve-out now admits an `int64` rank-1 `shared` output
named as another emitted value's `valid_lengths` as well as one named as an
offsets/owner, and §3.2, decisions §10.5/§10.6, matrix row 25, and P2's negative
fixtures follow. A `valid_lengths` companion needs no rebasing on delivery — a
length is already relative to the item it measures — so a request receives the
slice indexing its own items.

**A version may gate what a document must say; never what it may say wrongly.**
§6.1 gains the distinction, prompted by a rule that demanded an explicit append
axis and thereby rejected two shipped diffusion packages that meant the default
they were written against. When a new field makes an existing default ambiguous,
the demand to state intent explicitly starts at the version that introduced the
ambiguity. Range checks and every rule about a value a document did state stay
unconditional at every version — relaxing those lets an older spelling assert
something false rather than merely leave something unsaid. The two look alike in
a validator and are opposites in effect: one preserves packages that predate a
question, the other would preserve their bugs. Packages under the exemption
should still be migrated, so it stays a guarantee rather than a live path
nothing exercises.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…ed is not published

Two corrections to §4 rule 8 and its mirrors in decisions §10.5/§10.6, both
raised by the schema PR (#2009) after a review found the second of them as a
live contradiction in its validator.

**The carve-out's "rank 1" was wrong for a length.** An `offsets` and an `owner`
are rank 1 by construction, so the flat phrasing was accurate for them and read
as though it were the general rule. It is not: §3.2 fixes a `valid_lengths` at
the number of axes outer to the padded one, so a value padded on axis 2 publishes
a rank-2 length vector. Under the flat reading, one rule of this design requires
that companion and another refuses it — and the refusal necessarily advises
`request_aligned`, which is exactly what a companion must not declare. Following
the narrower wording would make a correct package wrong.

The condition is therefore per *reference*: collect what each declaration naming
the value requires of it — role, and the rank that role implies at that site —
and admit the value if it satisfies any one. Three consequences are written down
because each is a place an implementer must choose. Expectations accumulate
rather than overwrite, since one length vector may legitimately bound the same
dimension of two outputs. Two declarations demanding different ranks of one value
is a genuine contradiction, but it belongs to the padding declaration that is
wrong; raising it at the serving rule names an innocent line. And where a padded
dimension does not resolve to an axis, the reference carries no rank expectation
at all — that declaration is already being rejected, and a second complaint about
a rank nothing could compute sends the reader somewhere there is nothing to fix.
A value named as a companion but shaped like something else is refused as a
malformed companion, naming role and expected rank, rather than falling through
to the generic message that advises the one layout a companion may not have.

**Declared is not published.** Both halves of rule 8 now require the companion to
be emitted — written by some declared step — not merely to appear in the output
list. An output nothing writes hands the caller an empty vector beside a ragged
payload, which is the precise failure the rule exists to prevent wearing the
appearance of compliance. The check is whole-workflow rather than path-sensitive:
"written by some declared step" is what is soundly decidable without evaluating
branch predicates, and a path-sensitive rule would reject a correct package whose
companion is written in a sibling branch of the emit.

Acceptance row 25 grows the cases that distinguish the two readings: doubly
padded values publishing rank-1 and rank-2 lengths together, a packed value
padded on the axes it does not pack publishing three companions at three ranks, a
mis-ranked length, and a companion declared but written by no step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…d say why

A follow-on to the previous commit, closing the asymmetry the schema PR (#2009)
found on its own side of the same rule.

The obligation requires a companion to be emitted; the carve-out already required
the value that *names* it to be emitted, but the neighbouring decidability
sentence still said "decidable from the declared outputs alone", which invites the
weaker reading. Asking one side for an emit while accepting a bare declaration on
the other is two answers to one question (Rule 10), and the weaker half is
exploitable: declare a padded output beside any `shared` vector, write neither,
and the vector walks past the serving rule on the strength of a payload that does
not exist. Both halves now say emitted, the exploit is written down as the reason,
and an expectation is stated to carry both the declaration that claims the value
and whether anything writes that declaration.

The decidability claim is corrected rather than dropped: the condition is decided
from the workflow's own declarations — its outputs and its steps — with no
runtime information. That is what made the check affordable, and it survives the
stronger reading; only "outputs alone" was wrong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…xis`

The rank clause was corrected two commits ago, but only in the form "the number
of axes outer to the padded one". That is the definition; it is not the check,
and an implementer has to derive one from the other. The schema PR (#2009) states
the same fact as `rank == axis` because that is what its validator evaluates, and
the derivation is exactly where the earlier flat "rank 1" survived review on both
sides.

Both spellings are now given together, in §3.2 where the shape rule lives, in §4
rule 8's carve-out, and in decisions §10.6. No rule changes; a reader of any of
the three gets the check without translating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
PR #2009 merged as 0448f2b, which lands phases P1 and P2 of this design:
`WorkflowComponent.batch_capacity`, `TensorContract.padding`, the `levels`
ownership chain, the `pack_offsets` / `pack_owner` / `valid_lengths` content
roles, the `schema_version` gate, and the load-time validators for all of
them. The design of record was still written as if none of it existed, which
made its status line, its evidence section, and 57 of its source citations
false.

What changed:

- Status is `partly implemented` rather than `proposed`, naming which phases
  landed and which did not. The interpreter, preprocessor, scheduler, and
  backend phases remain unimplemented, so a package can now *declare* a
  batched encoder that no runtime will group; §6 already requires that
  asymmetry to be safe, and the note says so.
- §2 keeps its observations in the past tense and adds a `Since #2009` note
  per subsection. The observations are the record of why each field exists
  and are not recoverable from the field, so they are marked rather than
  deleted. §2.3, §2.4, and §2.5 are untouched because nothing closed them.
- §3.3 and the `valid_lengths` argument no longer claim the roles are absent
  from the vocabulary; they are declarable and nothing produces them, which
  is a different and still-open gap.
- §6.1's `schema_version` doc-comment defect is recorded as corrected, in the
  narrow form this design argued for: compatibility comes from a field being
  absent by default, which buys nothing for a field that was reshaped.
- §8 marks P1 and P2 as shipped while keeping the full obligation text, so
  the phase list stays reviewable against the design instead of decaying into
  a changelog.
- INFERENCE_METADATA_DECISIONS.md §10.4 becomes `What §10.1 did not have`,
  and §10.5 / §10.6 drop `proposed` for a status note that separates the
  load-time half now in force from the runtime half that is not. DESIGN.md
  and docs/README.md follow.
- 57 citations re-pinned across the two documents, 39 mechanically and 16 by
  hand where the merge moved the claim rather than the line. Several were
  in range but semantically wrong after the `Image*` to `Vision*` rename,
  which line-number repair alone would not have caught.

Two pre-existing defects found while checking: a link to a renamed §6 anchor,
and a short-form `redesign_invariants.rs:158` citation appearing before its
first full path. Both fixed.

Docs only. Verified: every intra-doc link and anchor resolves, every
citation is in range and reads as claimed, 28 acceptance rows with uniform
columns, `git diff --check` clean, lines within 88 columns.
`cargo test -p onnx-genai-metadata --test encoder_batching` passes 104 tests
at this main, which is the evidence for the P2 marker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…fy by content

#2009 merged as `0448f2bc6`, moving the files this document cites most. The
citations were re-pinned in an earlier commit on this branch, but the check
that guarded them asked only whether a line number fell inside the file. That
is a property of the file's *length*, not of its *content*: a citation can
point at unrelated code that reads plausibly and still pass, and the failure is
silent. Verification is now content-aware — for each of the 97 ranged
citations, the range's **first** line must contain the symbol or comment the
surrounding sentence claims, and the range must contain a second distinctive
substring. Anchoring on the head line makes the check position-sensitive, so
every citation fails under a one-line drift rather than passing.

Applying it to the merged tree found three references that were in range and
wrong at the head, all of them narrowings of a previously correct range:

- `schema/package.rs:158-174` -> `162-174`. The claim is about
  `batch_invariance`; lines 158-161 are the unrelated `decoding` field, so the
  citation opened on someone else's declaration.
- `ort/value.rs:1543-1551` -> `1544-1551`. The claim names
  `alias_with_offset`; 1543 is the closing line of the preceding doc comment.
- `ort/value.rs:1526-1542` -> `1524-1543`, in both documents. The claim quotes
  the no-copy-view doc comment; 1526 is a blank `///`. Re-pinned to the whole
  comment block, since a block citation cannot be off by one.

The remaining 54 ranges verify unchanged against the merged tree, including all
80 citations into the 24 files #2009 touched.

Records the convention in the header so the next editor re-pins by locating the
named symbol rather than by adjusting the number until it lands inside the file.

Docs only: no code, schema, or fixture changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
Two amendments adopted from the schema implementation (#2009), both places
where the implementation was stricter than this document and right to be.

**The packed axis rule now covers every packed value.** §4 rule 2 scoped
`axis == 0` to values a `batch_capacity` component consumes or produces. That
scope was too narrow: the runtime splits a packed value per request whenever it
hands one back — rule 8's emit path rebases offsets and derives per-request
owners with no capacity declaration in sight — and that split wants the same
contiguous span for the same aliasing reason. Under the old scope a strided
packed emit was legal precisely where nothing had declared it could pay for one.
No fixture in the tree declares a non-zero packed axis, so the wider rule
rejects nothing that exists.

**A preprocessing-program companion must carry the companion role.** §3.3
introduced `pack_offsets` and `pack_owner` but stated their use only
descriptively. §4 rule 4 now requires them of any level companion that is a
declared program output. The asymmetry with §3.2, where a length value is
resolved by name and its role is descriptive, is deliberate and now explained:
length vectors have established modality spellings that predate this design and
cannot be narrowed to one role, while the companion roles are new and have
nothing to accommodate — so requiring them costs nothing and catches a program
that wires, say, `grid_dimensions` into a `levels` entry at load rather than
shipping a plausible int64 rank-1 vector that means something else.

Decisions §10.5 and §10.6 restate both, matrix row 17 gains the two rejections,
and the P2 phase box's stale "§3.1 rules 1-7" is corrected to 1-9 to match the
body. No other rule, example, or acceptance row changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…hat a document must say

Two contract-visible consequences of the schema implementation's code review
(#2009), recorded here so the design of record does not lag the contract.

**A ragged emit publishes the account of its raggedness.** §4 rule 8 required a
packed emit to declare every level's `offsets` and `owner`, but said nothing
about a padded emit, which is the same hole. §3.2 rejects a payload-shaped
validity mask precisely so the length vector is the only account of the padding
that exists anywhere; a caller that receives the payload without it holds
trailing entries it cannot distinguish from real ones. Rule 8 is generalized to
cover both, the serving carve-out now admits an `int64` rank-1 `shared` output
named as another emitted value's `valid_lengths` as well as one named as an
offsets/owner, and §3.2, decisions §10.5/§10.6, matrix row 25, and P2's negative
fixtures follow. A `valid_lengths` companion needs no rebasing on delivery — a
length is already relative to the item it measures — so a request receives the
slice indexing its own items.

**A version may gate what a document must say; never what it may say wrongly.**
§6.1 gains the distinction, prompted by a rule that demanded an explicit append
axis and thereby rejected two shipped diffusion packages that meant the default
they were written against. When a new field makes an existing default ambiguous,
the demand to state intent explicitly starts at the version that introduced the
ambiguity. Range checks and every rule about a value a document did state stay
unconditional at every version — relaxing those lets an older spelling assert
something false rather than merely leave something unsaid. The two look alike in
a validator and are opposites in effect: one preserves packages that predate a
question, the other would preserve their bugs. Packages under the exemption
should still be migrated, so it stays a guarantee rather than a live path
nothing exercises.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…ed is not published

Two corrections to §4 rule 8 and its mirrors in decisions §10.5/§10.6, both
raised by the schema PR (#2009) after a review found the second of them as a
live contradiction in its validator.

**The carve-out's "rank 1" was wrong for a length.** An `offsets` and an `owner`
are rank 1 by construction, so the flat phrasing was accurate for them and read
as though it were the general rule. It is not: §3.2 fixes a `valid_lengths` at
the number of axes outer to the padded one, so a value padded on axis 2 publishes
a rank-2 length vector. Under the flat reading, one rule of this design requires
that companion and another refuses it — and the refusal necessarily advises
`request_aligned`, which is exactly what a companion must not declare. Following
the narrower wording would make a correct package wrong.

The condition is therefore per *reference*: collect what each declaration naming
the value requires of it — role, and the rank that role implies at that site —
and admit the value if it satisfies any one. Three consequences are written down
because each is a place an implementer must choose. Expectations accumulate
rather than overwrite, since one length vector may legitimately bound the same
dimension of two outputs. Two declarations demanding different ranks of one value
is a genuine contradiction, but it belongs to the padding declaration that is
wrong; raising it at the serving rule names an innocent line. And where a padded
dimension does not resolve to an axis, the reference carries no rank expectation
at all — that declaration is already being rejected, and a second complaint about
a rank nothing could compute sends the reader somewhere there is nothing to fix.
A value named as a companion but shaped like something else is refused as a
malformed companion, naming role and expected rank, rather than falling through
to the generic message that advises the one layout a companion may not have.

**Declared is not published.** Both halves of rule 8 now require the companion to
be emitted — written by some declared step — not merely to appear in the output
list. An output nothing writes hands the caller an empty vector beside a ragged
payload, which is the precise failure the rule exists to prevent wearing the
appearance of compliance. The check is whole-workflow rather than path-sensitive:
"written by some declared step" is what is soundly decidable without evaluating
branch predicates, and a path-sensitive rule would reject a correct package whose
companion is written in a sibling branch of the emit.

Acceptance row 25 grows the cases that distinguish the two readings: doubly
padded values publishing rank-1 and rank-2 lengths together, a packed value
padded on the axes it does not pack publishing three companions at three ranks, a
mis-ranked length, and a companion declared but written by no step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…d say why

A follow-on to the previous commit, closing the asymmetry the schema PR (#2009)
found on its own side of the same rule.

The obligation requires a companion to be emitted; the carve-out already required
the value that *names* it to be emitted, but the neighbouring decidability
sentence still said "decidable from the declared outputs alone", which invites the
weaker reading. Asking one side for an emit while accepting a bare declaration on
the other is two answers to one question (Rule 10), and the weaker half is
exploitable: declare a padded output beside any `shared` vector, write neither,
and the vector walks past the serving rule on the strength of a payload that does
not exist. Both halves now say emitted, the exploit is written down as the reason,
and an expectation is stated to carry both the declaration that claims the value
and whether anything writes that declaration.

The decidability claim is corrected rather than dropped: the condition is decided
from the workflow's own declarations — its outputs and its steps — with no
runtime information. That is what made the check affordable, and it survives the
stronger reading; only "outputs alone" was wrong.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…xis`

The rank clause was corrected two commits ago, but only in the form "the number
of axes outer to the padded one". That is the definition; it is not the check,
and an implementer has to derive one from the other. The schema PR (#2009) states
the same fact as `rank == axis` because that is what its validator evaluates, and
the derivation is exactly where the earlier flat "rank 1" survived review on both
sides.

Both spellings are now given together, in §3.2 where the shape rule lives, in §4
rule 8's carve-out, and in decisions §10.6. No rule changes; a reader of any of
the three gets the check without translating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
PR #2009 merged as 0448f2b, which lands phases P1 and P2 of this design:
`WorkflowComponent.batch_capacity`, `TensorContract.padding`, the `levels`
ownership chain, the `pack_offsets` / `pack_owner` / `valid_lengths` content
roles, the `schema_version` gate, and the load-time validators for all of
them. The design of record was still written as if none of it existed, which
made its status line, its evidence section, and 57 of its source citations
false.

What changed:

- Status is `partly implemented` rather than `proposed`, naming which phases
  landed and which did not. The interpreter, preprocessor, scheduler, and
  backend phases remain unimplemented, so a package can now *declare* a
  batched encoder that no runtime will group; §6 already requires that
  asymmetry to be safe, and the note says so.
- §2 keeps its observations in the past tense and adds a `Since #2009` note
  per subsection. The observations are the record of why each field exists
  and are not recoverable from the field, so they are marked rather than
  deleted. §2.3, §2.4, and §2.5 are untouched because nothing closed them.
- §3.3 and the `valid_lengths` argument no longer claim the roles are absent
  from the vocabulary; they are declarable and nothing produces them, which
  is a different and still-open gap.
- §6.1's `schema_version` doc-comment defect is recorded as corrected, in the
  narrow form this design argued for: compatibility comes from a field being
  absent by default, which buys nothing for a field that was reshaped.
- §8 marks P1 and P2 as shipped while keeping the full obligation text, so
  the phase list stays reviewable against the design instead of decaying into
  a changelog.
- INFERENCE_METADATA_DECISIONS.md §10.4 becomes `What §10.1 did not have`,
  and §10.5 / §10.6 drop `proposed` for a status note that separates the
  load-time half now in force from the runtime half that is not. DESIGN.md
  and docs/README.md follow.
- 57 citations re-pinned across the two documents, 39 mechanically and 16 by
  hand where the merge moved the claim rather than the line. Several were
  in range but semantically wrong after the `Image*` to `Vision*` rename,
  which line-number repair alone would not have caught.

Two pre-existing defects found while checking: a link to a renamed §6 anchor,
and a short-form `redesign_invariants.rs:158` citation appearing before its
first full path. Both fixed.

Docs only. Verified: every intra-doc link and anchor resolves, every
citation is in range and reads as claimed, 28 acceptance rows with uniform
columns, `git diff --check` clean, lines within 88 columns.
`cargo test -p onnx-genai-metadata --test encoder_batching` passes 104 tests
at this main, which is the evidence for the P2 marker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 25, 2026
…fy by content

#2009 merged as `0448f2bc6`, moving the files this document cites most. The
citations were re-pinned in an earlier commit on this branch, but the check
that guarded them asked only whether a line number fell inside the file. That
is a property of the file's *length*, not of its *content*: a citation can
point at unrelated code that reads plausibly and still pass, and the failure is
silent. Verification is now content-aware — for each of the 97 ranged
citations, the range's **first** line must contain the symbol or comment the
surrounding sentence claims, and the range must contain a second distinctive
substring. Anchoring on the head line makes the check position-sensitive, so
every citation fails under a one-line drift rather than passing.

Applying it to the merged tree found three references that were in range and
wrong at the head, all of them narrowings of a previously correct range:

- `schema/package.rs:158-174` -> `162-174`. The claim is about
  `batch_invariance`; lines 158-161 are the unrelated `decoding` field, so the
  citation opened on someone else's declaration.
- `ort/value.rs:1543-1551` -> `1544-1551`. The claim names
  `alias_with_offset`; 1543 is the closing line of the preceding doc comment.
- `ort/value.rs:1526-1542` -> `1524-1543`, in both documents. The claim quotes
  the no-copy-view doc comment; 1526 is a blank `///`. Re-pinned to the whole
  comment block, since a block citation cannot be off by one.

The remaining 54 ranges verify unchanged against the merged tree, including all
80 citations into the 24 files #2009 touched.

Records the convention in the header so the next editor re-pins by locating the
named symbol rather than by adjusting the number until it lands inside the file.

Docs only: no code, schema, or fixture changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 25, 2026
The batching design announced that the v1.1 gate shipped, then repeated pre-#2009 claims that schema_version was still unvalidated and cited lines that now document the opposite. Put the historical gap in past tense and point the normative section at the current normalized contract and pre-deserialization gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Copilot <223556219+Copilot@users.noreply.github.com>
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