# FEEDBACK — mgf-apiprobe

> Paper-tracking file for issues, friction, and requests against
> this sibling. Consumer projects (PlasmaMapper, inthewords,
> vm-vmanager-app, figrecovery, and any future adopter) file PAPER-NN
> entries here.
>
> **Where to file what:**
>
> - **mgf-apiprobe-specific** (probe shape, check behaviour,
>   matcher semantics, Suite execution) → file HERE.
> - **Federation-wide** (cross-sibling patterns, standards
>   proposals) → file in
>   [mgf-common/FEEDBACK.md](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md).
>
> **Numbering convention:** paper numbers (`PAPER-NN`) are
> local to this repo. When a paper cross-references a
> federation-wide paper, the federation number appears in the
> metadata table as a cross-reference row.
>
> **Discipline:** see
> [mgf-standard/docs/standards/DOCUMENTATION.md](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/DOCUMENTATION.md)
> §FEEDBACK (rules FB-01..FB-04, DOC-13).

---

## §1 Process

1. **File freely.** Any consumer hitting friction MUST file
   a PAPER-NN under §2. Don't wait for permission; the
   maintainer triages.
2. **Status flips** through `OPEN` → `SHIPPED` / `DEFERRED` /
   `WONTFIX` / `MOVED` as the paper resolves. A `SHIPPED` row
   carries a commit-SHA reference + release-version reference.
3. **Renumbering** never happens within this repo — papers
   keep their PAPER-NN forever (per FB-01).

---

## §2 Open feedback (active)

#### `PAPER-01` — No way to thread response data from one probe into another (auth or otherwise)

| Field | Value |
|---|---|
| Federation paper | [PAPER-39 in mgf-common](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md#paper-39) (original filing — sibling-scoped per DOC-13) |
| Status | ✅ SHIPPED |
| Severity | 🟡 High |
| Submitter | inthewords (commit [`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5)) |
| Submitted | 2026-05-18 |
| Decision | accept (general primitive — `Probe.depends_on(probe_id, extract={var: jsonpath})` + `{var}` template substitution in path / headers / queries — subsumes the auth case naturally; no dedicated `ChainedAuth` class needed) |
| Decided on | 2026-05-18 |
| Scheduled for | mgf-apiprobe next minor |
| Shipped in | mgf-apiprobe commit [`519ed9b`](https://codeberg.org/magogi-admin/mgf-apiprobe/commit/519ed9b) |

**Concern.** apiprobe's auth flows (`BearerAuth`, `ApiKeyAuth`,
`BasicAuth`, `CookieAuth`, `CustomAuth`) take **static**
credential inputs at construction time — token literal or env
var. None can express "probe 1 hits `/auth/token/` with
credentials → parse `$.token` from the response → use as
`Authorization` header for probes 2-N". `CustomAuth` is the
closest escape hatch but its
`prepare_func: Callable[[], AuthCredentials]` is synchronous,
zero-argument, and has no handle on the Suite's `Transport` or
`Context` — it cannot run another probe through the Suite's
machinery.

Generalising: there's no probe-to-probe data threading at all.
A "create resource, capture id, GET /resource/{id}/" flow can't
live inside a single Suite — the consumer pre-Suites the create
call outside apiprobe entirely.

**Why it matters.** Almost every real API has a credential-
exchange flow that can't be pre-baked into env vars (OAuth2
client_credentials, DRF token-obtain, session-cookie auth,
CSRF tokens). Any consumer adopting apiprobe must today bypass
it for the bootstrap probe. The bootstrap probe's findings then
sidestep apiprobe's check catalog, costing the typed-finding
visibility the library exists to provide.

**Concrete evidence.** inthewords' Suite at
`tools/apiprobe/suite.py` (commit
[`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5))
writes a pre-Suite `_obtain_token` httpx call. Costs paid:

1. The token endpoint is hit **twice** — once outside the Suite
   (to capture the token for `ApiKeyAuth`) and once inside
   (probe `token-obtain`, kept for contract verification but its
   returned token is unused).
2. The pre-Suite call bypasses apiprobe — no findings, no
   checks fire on it. If the credential exchange itself drifts
   (wrong content-type, missing rate-limit headers, slow
   response), apiprobe sees nothing.
3. The workaround is non-discoverable. Nothing in the README or
   `PUBLIC_API.md` says "this is the recommended shape." Each
   consumer reinvents.

The same pattern blocks general probe sequencing: inthewords'
`room-detail` probe hard-codes the seed slug `python-developers`
because there's no way to first POST `/rooms/` and capture the
returned slug.

**Suggested shape.** First-class probe-to-probe chaining:

```python
# A new AuthFlow variant — name TBD:
auth = ChainedAuth(
    obtain_probe=Probe.post('/auth/token/')
        .base(base_url)
        .id('auth-bootstrap')
        .body({'username': '...', 'password': '...'})
        .expect_status(200),
    extract='$.token',                  # jsonpath into the response
    header='Authorization',
    scheme='Token',                     # or value_template='Token {token}'
)

suite = Suite(probes=[
    Probe.get('/me/').with_auth(auth),
    Probe.get('/rooms/').with_auth(auth),
    ...
])
```

Same primitive generalises to non-auth data threading:

```python
create = Probe.post('/rooms/').body({...}).id('create-room').expect_status(201)
verify = (Probe.get('/rooms/{slug}/messages/')
          .depends_on('create-room', extract={'slug': '$.slug'})
          .id('verify-room'))
```

The Suite runs `obtain_probe` / dependency probes once, stores
extracted values in `Context`, and threads them into dependent
probes' headers / paths / bodies. The bootstrap probe runs
through the same Transport so its findings count, retry policy
applies, security checks fire.

**Decision rationale.** Ship the general primitive instead of a
dedicated `ChainedAuth` class. The submitter's filing proposed
both shapes (the focused `ChainedAuth` + the general
`Probe.depends_on`) but noted "Same primitive could underpin
Probe-to-Probe data threading more generally"; the implementation
takes that observation seriously. The general primitive
(`Probe.depends_on(probe_id, extract={var: jsonpath})` +
`{var}` template substitution in path / headers / queries)
subsumes the auth case naturally — no new AuthFlow class
needed, no extension to the AuthFlow Protocol (which would have
been mildly breaking), no special-case handling in the
Transport layer. The auth idiom becomes:

```python
auth = (Probe.post('/auth/token/')
        .id('auth-bootstrap')
        .body(creds)
        .expect_status(200))
profile = (Probe.get('/me/')
           .id('profile')
           .depends_on('auth-bootstrap', extract={'tok': '$.token'})
           .header('Authorization', 'Token {tok}'))
```

— and the same primitive handles create-then-verify, dynamic-
seed scenarios, and any future data-threading shape.

**Retrospective.** Shipped exactly as the submitter recommended,
collapsed onto a single primitive. Build:

- `ProbeDependency` frozen dataclass + `Probe.depends_on()` builder.
- `Context.with_added()` for run-time variable threading;
  Context stays frozen, callers thread forward.
- `resolve_template()` public helper in `mgf.apiprobe.core.context`
  for `{var}` substitution via `str.format_map` semantics.
- Suite-construction validation: dangling probe_id refs +
  cycles raise `SuiteLogicError` at suite-build time. Kahn's
  algorithm produces the topological order; output preserves
  input order when no deps exist (back-compat for the parallel
  path).
- `Suite.run` branches between the existing fast parallel-gather
  path (no deps anywhere) and a new serial topological executor
  (any probe has deps). The parallel path is unchanged — all
  806 existing tests still pass.
- Three CRITICAL synthetic Finding codes for failure modes:
  `suite.deps.skipped_due_to_upstream` (cascade),
  `suite.deps.extraction_failed` (body-not-json /
  jsonpath-no-match / parse-error),
  `suite.deps.template_unresolved` (missing `{var}` at runtime).
- `_run_one_probe` return signature widened to a 3-tuple
  `(findings, had_transport_error, response_or_None)` so the
  topological runner can cache responses for downstream
  extraction; the parallel-path call site unpacks and drops the
  third element.
- 23 new tests across builder / dataclass / Context /
  resolve_template / validation / topological happy paths /
  failure paths / back-compat. Total 806 → 829.
- New recipe `docs/recipes/how-to-chain-probes.md` covering the
  auth idiom + general data threading + each failure mode.

Out of scope (deferred to follow-ups):

- JSON body templating. Bodies are type-aware (dict/list); a
  string template against a serialised body has edge cases
  (substituted values that happen to be strings with quotes).
  v1 ships path / headers / queries only.
- Parallel-wave execution within the topological order. Probes
  whose deps all resolved in earlier waves could run in parallel
  within the next wave. v1 ships fully serial when ANY probe has
  deps; the perf delta is irrelevant for typical Suite sizes
  (handful of probes) but a worthy follow-up if a consumer ships
  a Suite with hundreds of probes.

---

#### `PAPER-02` — `BearerAuth` hardcodes the "Bearer" scheme; DRF and others need "Token" / custom

| Field | Value |
|---|---|
| Federation paper | [PAPER-40 in mgf-common](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md#paper-40) (original filing — sibling-scoped per DOC-13) |
| Status | ✅ SHIPPED |
| Severity | 🟢 Nice-to-have |
| Submitter | inthewords (commit [`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5)) |
| Submitted | 2026-05-18 |
| Decision | accept (suggested shape verbatim — `scheme=` kwarg with `"Bearer"` default) |
| Decided on | 2026-05-18 |
| Scheduled for | mgf-apiprobe next minor (bundled with PAPER-41 + PAPER-42 in one polish sweep) |
| Shipped in | mgf-apiprobe commit [`75187bc`](https://codeberg.org/magogi-admin/mgf-apiprobe/commit/75187bc) |

**Concern.** `BearerAuth(*, token=None, env=None)` always emits
`Authorization: Bearer <token>`. There is no way to override the
scheme word. Django REST Framework's stock token-auth scheme is
`Token` (literal); GitHub PATs use lowercase `token`; many
in-house APIs use bespoke schemes. The consumer falls back to
`ApiKeyAuth(header="Authorization", value=f"Token {token}")`,
which works but is semantically off — DRF tokens ARE bearer-
style credentials, just with a different scheme word.

**Why it matters.** Any non-OAuth API consumer hits this on
their first probe. The workaround reads weirdly in code review
("why is the DRF token configured as an `ApiKeyAuth`?") and the
PUBLIC_API.md description of `ApiKeyAuth` ("API-key flow.
Specify exactly one of `header=` or `query=`") doesn't quite
match what's happening (we're not passing an API key — we're
passing a bearer-shaped credential with a non-RFC scheme).

**Concrete evidence.** inthewords' Suite (`tools/apiprobe/
suite.py` line ~67, commit
[`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5)):

```python
auth = ApiKeyAuth(header='Authorization', value=f'Token {token}')
```

This works but warrants a comment because the type name doesn't
match the intent.

**Suggested shape.** Add a `scheme` parameter to `BearerAuth`:

```python
BearerAuth(*, token: str | None = None, env: str | None = None,
           scheme: str = 'Bearer')
```

`BearerAuth(token=t, scheme='Token')` reads naturally and the
emitted header is `Authorization: Token <t>`. Existing call
sites are unaffected (scheme defaults to `'Bearer'`). The
PUBLIC_API.md row stays one entry — no new AuthFlow class
needed.

Forward-compatible with PAPER-39: a future `ChainedAuth`
likewise needs a scheme word, and parameterising at the
BearerAuth layer means the same idiom carries through.

**Decision rationale.** Accepted verbatim (see Decision row) — the
`scheme=` kwarg with a `"Bearer"` default is additive and keeps
`BearerAuth` a single PUBLIC_API entry.

**Retrospective.** Shipped in `75187bc` as part of the PAPER-40/41/42
polish sweep.

- **Resolution (2026-07-10 queue sweep — verified in code):**
  `src/mgf/apiprobe/auth/flows.py:81` — `BearerAuth` takes the
  `scheme=` kwarg, default `"Bearer"` (RFC 6750), docstring shows
  `BearerAuth(token="abc-123", scheme="Token")  # → "Token abc-123"`.
  The scheme is validated at construction per RFC 7235's
  `auth-scheme` ABNF (non-empty, no whitespace/control chars);
  invalid schemes raise `ApiprobeConfigError`. Status stays SHIPPED.

---

#### `PAPER-03` — Default checks fire on inapplicable responses (e.g., `MissingHsts` on non-HTTPS)

| Field | Value |
|---|---|
| Federation paper | [PAPER-41 in mgf-common](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md#paper-41) (original filing — sibling-scoped per DOC-13) |
| Status | ✅ SHIPPED (Approach A + B + C) |
| Severity | 🟢 Nice-to-have |
| Submitter | inthewords (commit [`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5)) |
| Submitted | 2026-05-18 |
| Decision | accept Approach A (docs recipe) immediately; defer B (per-check applicability metadata) + C (finding-rollup view) as separate follow-ups |
| Decided on | 2026-05-18 |
| Scheduled for | A — bundled with PAPER-40 + PAPER-42 polish sweep; B / C — TBD |
| Shipped in | **A:** commit [`75187bc`](https://codeberg.org/magogi-admin/mgf-apiprobe/commit/75187bc) (`docs/recipes/how-to-tune-findings-for-local-dev.md`). **B + C:** PR `feedback/paper-03-c-rollup` — the declarative `Check.applies_to` applicability gate (B) + the `HumanReportWriter` finding-rollup (C). |

**Concern.** `DEFAULT_CHECKS` includes 18 checks, several of
which are context-dependent and raise WARN/INFO findings on
responses where the check doesn't conceptually apply:

- `MissingHsts` raises on any HTTPS-expected response that omits
  HSTS — but it also fires on `http://` responses where HSTS is
  meaningless.
- `MissingRateLimitHeaders` fires on every endpoint by default,
  but only rate-limited endpoints carry those headers. The vast
  majority of endpoints in any consumer's API are NOT rate-
  limited and thus would always emit this finding.
- `MissingServerHeader` is a security concern (the `Server`
  header is info disclosure), but the check NAME reads as
  "missing" while the finding intent is the opposite ("Server
  header present and revealing").

**Why it matters.** Consumers running the Suite against a local
dev stack (`http://localhost:8000`) hit 1 finding × N probes
worth of `MissingHsts` noise on day one. Without a documented
escape hatch, the consumer reads the report as "the library
generates lots of expected-fail findings" and either tunes them
out or stops trusting the high-signal ones.

**Concrete evidence.** inthewords' run against the dev stack
(`make apiprobe` against `http://localhost:8000`, commit
[`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5))
hit zero of these findings because the project's middleware
happens to set `Strict-Transport-Security`, `X-Frame-Options`,
`X-Content-Type-Options` even over http://. But a fresh project
without those middleware would receive 5 × `MissingHsts` + per-
unlimited-endpoint `MissingRateLimitHeaders` findings on the
first run.

**Suggested shape.** Three layers, ascending effort.

**Approach A — `severity_overrides` recipe.** Document the
existing `severity_overrides` parameter in a
`docs/recipes/dev-stack.md` recipe:

```python
suite = Suite(
    probes=[...],
    checks=list(DEFAULT_CHECKS),
    severity_overrides={
        'security.headers.missing-hsts': Severity.INFO,   # local dev http://
    },
)
```

Zero library change; pure docs.

**Approach B — per-check applicability metadata.**
`MissingHsts` auto-skips for non-HTTPS responses; the check
knows it doesn't apply. Same idea for
`MissingRateLimitHeaders` (opt-in only when the consumer marks
the probe as rate-limited via something like
`.expects_rate_limit_headers()`).

**Approach C — finding-rollup view in `HumanReportWriter`.**
"5x `observability.headers.missing-request-id` (probes: ...)"
rather than five identical entries. Independent quality-of-life
that helps every multi-probe Suite.

**Recommendation.** Ship A as a same-day docs PR, schedule B as
the right long-term shape, treat C as independent
HumanReportWriter polish.

**Decision rationale.** Accept A immediately (zero-cost docs recipe); C is independent `HumanReportWriter` polish that helps every multi-probe Suite, shipped as its own change; B (per-check applicability) is the bigger design change, deferred.

**Retrospective.** **A** shipped 2026-05-18 (the `severity_overrides` dev-stack recipe). **C** shipped 2026-07-01: `HumanReportWriter` now rolls up findings that render identically (same code / message / remediation / threat_refs / root_cause) across N probes into a single block headed by the distinct-probe count + list (capped at 8, tail collapsed to `+M more`), instead of N identical entries — e.g. `observability.headers.missing-request-id (5 probes: probe-0, …)`. `Report.findings`, the JSON writer, and every breakdown are untouched (display-only). Regression tests added; full 867-test suite green. **B** shipped 2026-07-01: formalized per-check applicability as a declarative `Check.applies_to(probe, response, ctx)` hook (default `True`) that the suite consults before running each check — a non-applicable check is skipped entirely (no finding, not even a passing one). The pre-existing ad-hoc inline guards moved into it: `MissingHsts` (HTTPS-only), `MissingRateLimitHeaders` (429-only). Verification note: those two guards **already existed** in current code (the 2026-05-18 paper predated them), and the remaining hygiene checks correctly stay scheme-agnostic (`X-Content-Type-Options` / `X-Frame-Options` matter on `http://` too), so no other check needed a guard — B's value is making applicability **first-class + introspectable**, not new skips. Integration + unit tests added; full 871-test suite green.

---

#### `PAPER-04` — `Probe.id()` is mandatory for readable reports but not surfaced as such

| Field | Value |
|---|---|
| Federation paper | [PAPER-42 in mgf-common](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md#paper-42) (original filing — sibling-scoped per DOC-13) |
| Status | ✅ SHIPPED (Approach A) |
| Severity | 🟢 Nice-to-have |
| Submitter | inthewords (commit [`093e5d5`](https://codeberg.org/magogi-admin/inthewords/commit/093e5d5)) |
| Submitted | 2026-05-18 |
| Decision | accept Approach A (auto-derive from method + path in `Probe._new`); Approach B (README quickstart note) folded into the same commit |
| Decided on | 2026-05-18 |
| Scheduled for | bundled with PAPER-40 + PAPER-41 in one polish sweep |
| Shipped in | mgf-apiprobe commit [`75187bc`](https://codeberg.org/magogi-admin/mgf-apiprobe/commit/75187bc) |

**Concern.** Findings render with `probe: <id>` if the consumer
called `.id("name")` on the probe; without it, findings reference
the probe by a generated label that's harder to scan. The
README quickstart shows `.id()` in passing but doesn't call out
that report readability hinges on it.

**Why it matters.** The HumanReportWriter is the consumer's
first surface — when a finding fires, the consumer scans for
"which probe?" before "what's wrong?". A consumer following the
quickstart literally (no `.id()`) gets a noisier report on day
one, mistrusts the output, and may abandon the library before
discovering the fix.

**Concrete evidence.** inthewords' Suite explicitly calls
`.id("token-obtain")`, `.id("profile")`, etc. for each of the 5
probes — instinct from the README, not because anything said
"you must". The `make apiprobe` output is readable BECAUSE of
this:

```
WARN — 5
  observability.headers.missing-request-id (probe: token-obtain)
  observability.headers.missing-request-id (probe: profile)
  observability.headers.missing-request-id (probe: rooms-list)
  observability.headers.missing-request-id (probe: room-detail)
  observability.headers.missing-request-id (probe: profile-unauth)
```

Without `.id()` calls, the `(probe: X)` clause would be opaque.

**Suggested shape.** Two non-exclusive moves.

**Approach A — auto-derive a default id from method + path.**
e.g. `GET_api_v1_me_profile_` when `.id()` isn't called. Always
beats opaque numbering. Consumers who care about meaningful
names override via `.id()`.

**Approach B — flag in README quickstart.** One-line addition
near the existing `.id()` reference: "Without `.id()`, findings
reference the probe by a generated label — set an id for
readable reports."

**Recommendation.** Both. A makes the failure mode soft; B
removes the surprise.

**Decision rationale.** Both approaches accepted (see Decision row):
A (auto-derive) makes the failure mode soft; B (README note) removes
the surprise. Folded into one commit.

**Retrospective.** Shipped in `75187bc` as part of the PAPER-40/41/42
polish sweep.

- **Resolution (2026-07-10 queue sweep — verified in code):**
  `src/mgf/apiprobe/core/probe.py:176-192` — `Probe._new` derives a
  default probe_id via `_derive_default_id(method, path)` (defined at
  `probe.py:452`), with an inline `# PAPER-42` comment marking the
  provenance. Consumers who never call `.id()` get a readable
  method+path-derived label instead of an opaque one. Status stays
  SHIPPED.

---


## §3 Reviewer's meta-feedback

### From vm-vmanager-app (CLI/desktop consumer of mgf-common) — 2026-05-18

Cross-cutting observation derived from vm-vmanager-app's `0.34 → 0.38`
catch-up to mgf-common ([commit `212808f`](https://codeberg.org/magogi-admin/vm-vmanager-app/commit/212808f),
2026-05-18). I am NOT an mgf-apiprobe consumer.

**The observation: vm-vmanager-app already has a probe-shaped module.**
vm-vmanager-app ships `src/vmanager/monitor/health.py` — per-VM health
probing (TCP socket-open + httpx GET against guest-installed
HTTP endpoints, with SC-07 finite-timeout discipline). When
mgf-apiprobe matured enough to be worth evaluating, the
consumer-side question I couldn't quickly answer was: "is this
the right home for what I already have?" — i.e., does
mgf-apiprobe target the same threat-model (per-guest probing
from a desktop orchestrator) as my existing module, or is it
SaaS-side liveness/readiness for a service handing out HTTP
endpoints?

The migration question is non-trivial for consumers in this
shape: the existing module is tested (live in CI for 1907+
tests), the test surface is large, and the abstraction shape
might or might not match. Without clear "migrate IF" / "don't
migrate IF" guidance, the consumer-side decision defaults to
"keep what works."

**Implication for mgf-apiprobe.** Publish a "should you migrate
to apiprobe?" checklist in the README:

- **Migrate IF**: (a) your probes are SaaS-side liveness /
  readiness for a service you operate; (b) you want the
  health-aggregation shape from `mgf.common.health` (0.37+); (c)
  you have multiple consumers of the same probe and want a
  standard ABC.
- **Don't migrate IF**: (a) your probes are per-guest /
  per-customer-asset checks where the threat-model is "is this
  thing I'm orchestrating responsive?" rather than "is the
  service I'm offering up?"; (b) you have existing tested code
  that doesn't fit the apiprobe abstraction.
- **Reference consumers**: name 2-3 actual federation consumers
  who fit each side. (vm-vmanager-app fits the "don't migrate" side —
  monitor/health.py stays put.)

The federation contract now pressures every consumer at every
adoption gate to evaluate every sibling. A migration checklist
turns "should I migrate?" from an open-ended evaluation into a
yes-or-no check.

---

## §4 How consumers submit feedback

1. **Open a Codeberg issue** at
   <https://codeberg.org/magogi-admin/mgf-apiprobe/issues> if
   you have a quick question. Maintainer triages.
2. **For a structured pain point**: file a PAPER-NN here as a PR
   that adds the entry to §2. Use the existing entries as the
   shape template.
3. **For AI-agent-driven consumer sessions**: see your project's
   `docs/CLAUDE.md` auto-block (managed by `mgf-common catch-up`)
   — the `feedback-discipline` section spells out where to file
   what.

---

## §5 Cross-references

- [mgf-common/FEEDBACK.md](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
  — federation-wide PAPERs.
- [mgf-standard/docs/standards/DOCUMENTATION.md](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/DOCUMENTATION.md)
  — FEEDBACK discipline rules (FB-01..FB-04, DOC-13).
- [mgf-common/docs/recipes/feedback-md-template.md](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/recipes/feedback-md-template.md)
  — the canonical shape this file was built from.

> **Routing (DOC-10):** this FEEDBACK.md is the **consumer-facing** inbox for mgf-apiprobe-specific signal (probes, checks, the CLI). Internal work-in-progress posture lives in `docs/inprogress/`; federation-wide signal goes to `mgf-common`'s FEEDBACK.md.
