Skip to content

transcribe phase B: the stack table, the planner, and every refusal #20

Description

@fyang0507

Context

Decomposes #12, phase B of
TRANSCRIBE_IMPLEMENTATION_PLAN.md.

The capability half of the stack table is prose in two documents today. It has to become data,
and that data is what capabilities and plan serialise. This phase is pure functions over that
table plus a metadata probe, so it ships two commands and every refusal without a single model
being loaded.

Scope

stacks.json, stacks.py, planner.py, refusals.py, catalog.py, plan.py, and
audio transcribe capabilities / audio transcribe plan. Neither reads media beyond
probe_media, neither provisions anything, neither touches the network.

The table

One cell per (stack, capability), resolving to exactly one of five values, plus the per-stack
columns the catalog needs: family, environment, the roles sentence, unit and unit-count rule,
failure recovery, cost.proved with its rate and fixture, the language vocabulary, and the
one-line characterization stack_required prints.

Cell Catalog availability Plan satisfaction Requesting it
native native native adds nothing
native_stage:<stage> native native with stage adds cost, not composition
add_on:<package> requires_add_on derived with backend adds a role and a package
unsatisfiable_on_stack impossible + reason exit 2, allowed non-empty
unsupported impossible + reason exit 2, allowed: []

Both refusal codes fall out of whether allowed is empty, which is what keeps them from being
rendered alike. VOCABULARY's derivation table stays the source of truth and the test is
parametrized over it.

stacks.json is a separate file from manifest.json, deliberately: environments/__init__.py
states in its own docstring that the capability half is not there, and two files with a
cross-check test — every stack named in one appears in the other, every package a cell names
exists — is two owners of one table rather than two tables.

--language

Accepted on the two Qwen stacks only, as a closed enum of the 30 declared names, matched
case-insensitively, passed through verbatim. _build_prompt falls back to the raw string when a
name misses (mlx-audio 0.4.5 qwen3_asr.py:926-929, guarded by the manifest's source hash), so
--language EN would interpolate language EN<asr_text> into the prompt with no error — a flag
that parses and silently changes the decode. The accepted set is closed and readable from
config.json:support_languages, identical in both pinned checkpoints, so refusing needs no
translation table and no alias map. did_you_mean turns EN into a suggestion the caller
retypes, never a silent resolution.

The hint never reaches the aligner; see the Qwen phase issue for why.

Refusals

All thirteen, printed bare on stderr as the documents show. The shipped commands keep their
{"error": …} envelope; the divergence is recorded in the plan document rather than silently
becoming a third convention.

Out of scope

run, the transport, adapters, any stage script. sample_output comes from the phase-A
serializer; if that is not merged, this phase is blocked on it.

Acceptance

  • capabilities and plan output diffed key-for-key against HAPPY_PATH §1.1, §1.2, §2.1 and
    §3.1 using the shape() comparison tests/test_shipped_commands_match_the_document.py already
    defines, extended to the new commands. That comparison is the one that caught three shipped
    commands drifting from the document.
  • Every refusal in HAPPY_PATH §4.1–4.7 and CONTRACT §5 reproduced field-for-field against the
    per-code table, with capability_unknown, capability_unsatisfiable_on_stack, and
    capability_unsupported distinct rather than collapsed.
  • --language EN returns option_value_unsupported with did_you_mean: "English";
    --language english resolves to English; --language Cantonese on vibevoice returns
    option_unsupported_on_stack with stacks_accepting: ["qwen-1.7b", "qwen-0.6b"].
  • The 30 names equal the provisioned checkpoint's support_languages when the mlx
    environment exists, and the test skips when it does not — the pattern the private-API hash and
    the byte counts already use.
  • A plan carries no outcomes, and an omitted --want resolves the floors-only request
    rather than the menu.
  • Exit 2 for every request error happens before any provisioning check, so no refusal can
    reach exit 3 first.
  • uv run --extra dev pytest green.

Depends on

  • #18option_value_unsupported, the 30 accepted language names, and the corrected §3.1 residency all land there.
  • #19sample_output is that serializer; this phase cannot ship without it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions