Skip to content

Capability facts as a contract: apr-model-capability-v1 + SHACL + exposure on verb/http/mcp (operator keystone) #3856

Closed as not planned

Description

@noahgift

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 surfaced IQ1_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_OPS added: 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. GeluMlp is the MLP block path; gelu_host wraps the GELU activation, which is wired. aprender-5e warned about exactly this trap, having hit and refuted it themselves. Now unestablished.

done_when

Refs: #3075, #3077, #3418, #3421, #3686, #3850, #3852, #3853

Activity

  1. added 5 commits that reference this issue on Sep 22, 2026
  2. added a commit that references this issue on Sep 22, 2026
  3. noahgift commented on Sep 22, 2026

    @noahgift
    ContributorAuthor

    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-serve is the natural home

    The router registers 20 paths (crates/aprender-serve/src/api/router.rs), including /api/show, /api/version and /v1/gpu/status — so a capability endpoint sits beside existing introspection rather than introducing a new concept.

    aprender-serve already owns capability.rs, and it already has its own crates/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, a GET /v1/capability route 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)$). A GET /v1/capability is not one, so it will not be probed by the ladder. The HTTP surface therefore needs its e2e coverage in e2e_http_serve_t explicitly — and that is the right place, since interface-parity checks reachability through exactly those targets.

    MCP — blocked on a decision, and this is the finding

    $ sed -n '/^\[dependencies\]/,/^\[/p' crates/aprender-mcp/Cargo.toml
    serde, serde_json, anyhow (optional), inventory, pmcp (optional)
    
    $ grep -c 'path = "\.\./' crates/aprender-mcp/Cargo.toml
    0
    

    aprender-mcp has ZERO in-workspace path dependencies. It is deliberately standalone. Its tool registration is inventory-backed (ToolIndex::from_inventory, server.rs:678), so adding a tool is genuinely one file under src/tools/ — but the data is the problem, not the registration.

    Two options, and neither is free:

    (a) Give aprender-mcp its 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-mcp a 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 thin aprender-capability crate owning the contract, its parser, and one mirror, depended on by apr-cli, aprender-serve and aprender-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

    • decide (a) vs (b) before implementing — this is the whole cost
    • if (b): a thin capability crate; apr-cli's mirror and its equality test move there, and the existing arrangement shrinks rather than growing
    • GET /v1/capability on the router, handler reading the registry
    • e2e coverage in e2e_http_serve_t — the ladder's serve probe will not cover it (generation routes only)
    • one #[mcp_tool] file under crates/aprender-mcp/src/tools/
    • transport-decl / transport-absence need no change: cli, mcp and http are already declared in the root Cargo.toml, and neither surface adds a subcommand

    Refs: #3860, #3862

  4. added 3 commits that reference this issue on Sep 22, 2026
  5. added this to the 0.71.0 milestone on Sep 23, 2026
  6. noahgift commented on Sep 23, 2026

    @noahgift
    ContributorAuthor

    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

  7. modified the milestones: 0.71.0, 0.70.0 on Sep 23, 2026
  8. 11 remaining items

  9. modified the milestones: 0.70.0, 0.71.0 on Oct 4, 2026
  10. modified the milestones: 0.71.0, backlog on Oct 14, 2026
  11. noahgift commented on Oct 14, 2026

    @noahgift
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityont-0.70Ontology spec work that must finish in 0.70 (operator 2026-09-23)owner:aprender-a7owner sessionsize:MK̂ 3-4 h

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions