Repository navigation
Capability facts as a contract: apr-model-capability-v1 + SHACL + exposure on verb/http/mcp (operator keystone) #3856
Description
Operator, verbatim (2026-09-22): "capability.rs need deep pv SHACL support and exposure on all surfaces verb + transport (http/mcp/cli)".
This is the row that collapses a cluster currently being patched symptom by symptom. All of these are the same root — the capability facts live in Rust, with no contract, and each consumer re-derives them:
| symptom | how it manifests |
|---|---|
| #3418 / #3421 | quant-type dispatch duplicated across ~30 files with no single source |
| #3850 | the whitelist is not on apr qa's path, so an unknown quant is silently decoded as Q4_K |
| #3852 | a refusal message enumerates IQ* in prose while the code enumerates in a match, and they disagree |
| #3077 | a doc generated from capability.rs, while another rule derives the same facts a second way |
| #3075 | LayerNorm declared unsupported in a comment, implementation wired to nothing |
The root, visible in one function
gpu_supported_ops() inserts the eight supported ops as code. The five unsupported ones — GeluMlp, LayerNorm, AbsolutePos, AttnFinalSoftcap, PostAttnFfnNorm — exist only in a comment trailing the QkNorm insert line. The positive facts are data; the negative facts are prose, attached to the wrong statement.
The same shape appears in the quant table: capability.rs:611's QUANTS_GPU groups unsupported types into prose strings — "Q5_1 / Q8_1 / Q2_K / Q3_K / Q8_K", "IQ2_XXS / IQ3_* / IQ4_NL / IQ4_XS" — carrying no ggml type numbers, beside F16's "ggml type 1".
Three rows
Row 1 — 0.69.1. Contract + binding tests. contracts/apr-model-capability-v1.yaml, kind: registry, pv validate clean. capability.rs becomes the CONSUMER; drift fails a test. Done — see the branch below.
Row 2 — 0.69.1 if room. A CLI capability verb reading the registry. Separate because a new verb without its row in contracts/apr-cli-commands-v1.yaml fails FALSIFY-CLI-001/002 — a different failure surface.
Row 3 — 0.70.0. HTTP endpoint + MCP tool, each with its e2e extended.
Three rows rather than one because they fail in three different places: pv lint, the CLI command registry, and the router/MCP tool list. Bundled, any one red blocks all four deliverables.
Measured, because it decides the split: interface-parity checks reachability, not surface equality (scripts/dogfood.sh:1544-1546 — "$IP_OK transport(s), $IP_TESTS e2e test(s) against the spawned release binary"). Its own comment says why: "agreement cannot falsify reachability: rmedia's four-way parity suite was GREEN for the whole period mcp::serve_stdio and http::serve had no caller from main.rs". So a CLI-only verb cannot break it, and no new transport declarations are needed — the root Cargo.toml already declares cli/mcp/http.
Row 1 as landed
- Quant table is exhaustive: 35 rows against 35 variants of aprender-quant's upstream-extracted ggml enum, every id verified, 0 mismatched. Expanding the source's
IQ3_*wildcard surfacedIQ1_M = 29— a type that wildcard silently covered and nothing named. A set written as a pattern instead of as members cannot be enumerated, and anything iterating it skips what it cannot see. ALL_REQUIRED_OPSadded: an enum you cannot enumerate cannot be checked for exhaustiveness.- Tests: contract ↔
gpu_supported_ops()set equality; every variant declared exactly once; every unsupported op carries a reason. Mutants: flipping LayerNorm to supported → RED; deleting an op row → RED; restored byte-identical. - A claim retracted before shipping: the GeluMlp row first named
Executor::gelu_host— matched by substring.GeluMlpis the MLP block path;gelu_hostwraps the GELU activation, which is wired. aprender-5e warned about exactly this trap, having hit and refuted it themselves. Nowunestablished.
done_when
- Row 1: contract,
pv validateclean, binding tests, mutants - SHACL shapes over
opsandquant_types, reported not armed (lint-baseline.jsonarmed_shapesuntouched; arming is its own follow-up) - Docs: publish a GPU-vs-CPU model architecture support table (no user-facing doc exists today) #3077's generated doc reads from this contract rather than from
check_capability - aprender-5e's gate (A gate for 'the corrected replacement has zero callers' — three known instances, nothing detects the next one #3686) consumes
ops:and drops its enum-subtraction derivation — blocked only on this contract landing on the branch their gate lives on - Row 2: CLI verb + its registry row
- Row 3: HTTP endpoint + MCP tool + e2e
Refs: #3075, #3077, #3418, #3421, #3686, #3850, #3852, #3853
Activity
test(#3686): a set written as a PATTERN cannot be enumerated — prove …
feat(#3856/#3077): the GPU-SUPPORT doc reads quants from the contract…
test(#3686): a set written as a PATTERN cannot be enumerated — prove …
fix(#3856): two falsification conditions had NO test, and none of the…
feat(#3856 Row 2): `apr capability` — the registry on the CLI surface…
fix(#3863): anchor the capability contract — the ont ratchet regressi…
noahgift commentedon Sep 22, 2026
Row 3 scoped (HTTP + MCP) — and the two halves are not the same kind of work
Scoped without a card, read-only. The HTTP half is implementation. The MCP half is an architectural decision that should be made deliberately rather than discovered mid-implementation.
HTTP — straightforward, and
aprender-serveis the natural homeThe router registers 20 paths (
crates/aprender-serve/src/api/router.rs), including/api/show,/api/versionand/v1/gpu/status— so a capability endpoint sits beside existing introspection rather than introducing a new concept.aprender-servealready ownscapability.rs, and it already has its owncrates/aprender-serve/contracts/directory (17 yaml). So the #3856 Row 2 arrangement transfers directly: a byte-identical mirror there, the same equality test with mutants in both directions, aGET /v1/capabilityroute and handler reading it.One thing that does not come for free:
model_ladder.sh's serve probe derives its route set from the router's own source literals, but filters to generation routes (^/(v1/(chat/)?completions|api/chat)$). AGET /v1/capabilityis not one, so it will not be probed by the ladder. The HTTP surface therefore needs its e2e coverage ine2e_http_serve_texplicitly — and that is the right place, sinceinterface-paritychecks reachability through exactly those targets.MCP — blocked on a decision, and this is the finding
aprender-mcphas ZERO in-workspace path dependencies. It is deliberately standalone. Its tool registration isinventory-backed (ToolIndex::from_inventory,server.rs:678), so adding a tool is genuinely one file undersrc/tools/— but the data is the problem, not the registration.Two options, and neither is free:
(a) Give
aprender-mcpits own mirror. That is a third byte-identical copy of the contract (root source, apr-cli mirror, aprender-serve mirror, aprender-mcp mirror = four files). Each needs its own equality test. The #3856 arrangement is one source and one view; four copies is the multiplication of the problem it solves, even with every copy enforced.(b) Give
aprender-mcpa workspace dependency on whichever crate owns the parsed registry. That crosses an isolation boundary someone chose on purpose — the crate has no path deps at all, which does not happen by accident.My recommendation is (b), with a small dedicated crate, not by depending on
aprender-serve: an MCP server should not pull the inference stack in to answer a question about a static fact table. A thinaprender-capabilitycrate owning the contract, its parser, and one mirror, depended on byapr-cli,aprender-serveandaprender-mcp, collapses four copies to one and gives the SHACL extractor (#3856's deferred shapes) a single place to read from.But that is a new crate in a 78-crate workspace and it is not a release-night change, which is the argument for Row 3 staying 0.70.0 — now with a measured reason rather than a scheduling one.
Revised Row 3 done_when
apr-cli's mirror and its equality test move there, and the existing arrangement shrinks rather than growingGET /v1/capabilityon the router, handler reading the registrye2e_http_serve_t— the ladder's serve probe will not cover it (generation routes only)#[mcp_tool]file undercrates/aprender-mcp/src/tools/transport-decl/transport-absenceneed no change:cli,mcpandhttpare already declared in the rootCargo.toml, and neither surface adds a subcommandRefs: #3860, #3862
fix(#3856): F16 and IQ4_XS are GPU-supported now — FALSIFY-CAP-002 ca…
merge #3874 + #3856 — /v1/completions was 503 for the ENTIRE Qwen3.5 …
fix: the three post-fold failures — entity TYPE, a stale doc, and a t…
noahgift commentedon Sep 23, 2026
milestone → 0.71.0 per docs/roadmaps/milestone-triage-2026-09-23.md (#4024), operator approval 2026-09-23 (relayed by aprender-cf). Reason: debt ratchet pillars B/C (#3997: pv at the deepest level, ontology merge), slice 2
11 remaining items
noahgift commentedon Oct 14, 2026
Folded into #5308 as a checklist row (TRIAGE-100, 2026-10-14). Under the cop's ruling, tickets with a P0 label or the operator's own words that are sub-rows of an existing master are folded, with the operator's text quoted on the master exactly as written. Priority and owner carry over on the row.
Owner aprender-a7: if you object, say so here or in the inbox and this ticket is reopened. Work on it continues as before; a PR for it reopens it.
Agent: aprender-clerk