docs(tasks): decompose review-002 into 24 tasks (23 implementation + 1 bracketed follow-up)

Decomposition of docs/reviews/002-post-remediation-review.md per its
5-unit remediation plan:

- Unit 1 (security-critical): gw15-publish-body-cap,
  prj16-schema-via-call (CF-004 filed alkcall-side), fwd13-dot-segments,
  fwd16-missing-capability, oai11-ref-memoization
- Unit 2 (timeout/terminality): ws13-idle-progress,
  fwd15-stream-timeout, cli01-retry-after-budget, con17-mcp-pagination,
  con18-wss-sweep-exit
- Unit 3 (projection/docs): projection-truthfulness, mcp-batch-cap,
  gw16-status-drift
- Unit 4 (spec-import): yaml-normalization, oai13-path-item-wildcards,
  import-loudness-cluster, js01-placeholder-check,
  fwd17-19-contract-decisions
- Unit 5 (WS polish + tests): con18b-ws-polish,
  client-policy-wire-tests, cov-deployment-knobs, cov13-dead-code,
  srv11-srv12-router-ordering
- review-002-bracketed-followup: tentatively planned post-bulk pass
  (stale-check, OQA-18 enforcement decision, CON-08/09 close() lever,
  cross-crate re-checks) — deliberately not serialized against the
  bulk

Also: review-002 numbering repair (CON-14 was double-booked; MCP
pagination now CON-14, from_wss monitor renumbered CON-18, missing
CON-14 section added).

taskgraph: 66 valid, no cycles; 24 pending (all review-002);
gen-1/gen-2 parallel waves identified; workflow-cost hotspots are
prj16 (12.8) and ws13 (11.1), both carrying the reviewed slicing
guidance in their Notes.
This commit is contained in:
2026-08-30 10:50:34 +00:00
parent 5452785f0d
commit e2c255d40c
25 changed files with 1679 additions and 10 deletions
@@ -0,0 +1,56 @@
---
id: review-002-con17-mcp-pagination
name: Bound MCP tools/list pagination (max pages + deadline) (CON-14)
status: pending
depends_on: []
scope: narrow
risk: low
impact: component
level: implementation
tags: [adapters, review-002, mcp]
---
## Description
Review 002 CON-14 [major]. `from_mcp/mod.rs:114-119` delegates to
rmcp 1.8.0's `Peer::list_all_tools`, which loops `while
cursor.is_some()` with **no page cap and no timeout** (verified against
rmcp source `service/client.rs:390-407`). A malicious or buggy server
that always returns `next_cursor: Some(...)` (cycling or ignoring the
cursor) hangs `import()` indefinitely while appending every page's
tools → unbounded memory growth alongside the hang. Tools from
completed pages are not lost (extend per page), but nothing in alkhttp
bounds the loop. The paging test that exists (from_mcp_integration.rs)
terminates after 3 pages — the cycling shape is untested.
## Acceptance Criteria
- [ ] Replace `list_all_tools` with a bounded loop over `list_tools`:
hard page-count cap + overall deadline (constants or config;
pick generous defaults — e.g. 100 pages / 60 s — and note them
in the module doc)
- [ ] Budget trips → `AdapterError::DiscoveryFailed`-family error
naming the budget (pages seen, tools accumulated), no partial
import left behind (the existing import-fails-closed behavior
holds)
- [ ] Integration test: a paging server that cycles its cursor
(always `next_cursor: Some("a")`) → import terminates with the
clean error, bounded time (no hang)
- [ ] Existing 3-page pagination test unchanged and passing
(well-formed pagination still imports all pages)
- [ ] `cargo test --features mcp`, `cargo clippy --all-features --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part F', CON-14)
- src/adapters/from_mcp/mod.rs:114-119 (the delegation), rmcp 1.8.0 src/service/client.rs:390-407 (the unbounded loop)
- tests/from_mcp_integration.rs:302-317 (the existing paging server)
- tasks/adapters/review-001-consumer-adapter-hygiene.md (CON-01's pagination work this bounds)
## Notes
Small, self-contained, feature-gated (`mcp`). Consider (optional) a
`max_tools` sanity cap as well — same budget family, one more guard
against a hostile server — but do not let scope creep: the page cap +
deadline closes the hang, which is the finding.
@@ -0,0 +1,59 @@
---
id: review-002-con18-wss-sweep-exit
name: Dead-connection fast-fail + bounded from_wss sweep exit (CON-18)
status: pending
depends_on: []
scope: narrow
risk: low
impact: component
level: implementation
tags: [adapters, review-002, from-wss]
---
## Description
Review 002 CON-18 [minor]. The review-001 CON-02 fix added a 1 s
pending-map sweep to `from_wss`'s drop monitor — but it never exits:
once `eof_observed` holds, the sweep branch runs `fail_all` every
second **forever** (`from_wss.rs:254-272`). Each fire-and-forget
`import()` whose peer dies leaves a spawned task + pending map + watch
receiver alive for the process lifetime (a true task/allocator leak
scaling with import count). Note the naive fix is wrong: the sweep
test proves post-EOF registrations race `fail_all` and hang without
the sweep — so exit requires making post-EOF registration fail fast
*first*.
## Acceptance Criteria
- [ ] Post-EOF (dead connection) registration fails fast: calls
registered after the observed EOF resolve immediately with
`CONNECTION_CLOSED` (retryable, matching the established
mapping) instead of relying on the next sweep tick
- [ ] The sweep then exits after a bounded grace period following EOF
(long enough that the fast-fail path is exercised; not forever)
— no per-dead-session permanent task remains
- [ ] Re-verify the existing race tests still pass (pre-drop,
forgotten/held drop-during-registration, post-EOF sweep —
from_wss.rs:809-1001) and tighten assertions where the fast-fail
makes timing deterministic
- [ ] A task-lifecycle test (or task-count assertion) proving the
monitor ends after teardown
- [ ] `cargo test --features wss`, `cargo clippy --features wss --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part B', CON-18 / CON-16-withdrawn merge note)
- src/adapters/from_wss.rs:244-273 (the monitor loop), :75 (interval), :305 (fire-and-forget import)
- tasks/adapters/review-001-ws-eof-signal.md (the WS-02/CON-02 fix this bounds)
- alkcall/docs/reviews/consumer-findings-ledger.md CF-001 (the retryability classification — keep consistent)
## Notes
Implementation shape suggestion: an `AtomicBool`/watch-dead flag on
the call connection consulted in the registration path (fail-fast)
plus a sweep-generation counter (exit after N idle sweeps post-EOF).
The alkcall-side CF-001 note matters: if/when the dead-mux write
mapping changes, the fail-fast class here should match. Keep the
WS-02 losslessness invariant intact — fast-fail is an *additional*
resolution path, not a replacement for the watch signal.
@@ -0,0 +1,67 @@
---
id: review-002-fwd13-dot-segments
name: Reject lone dot-dot path values that Url-set_path normalizes away (FWD-13)
status: pending
depends_on: []
scope: narrow
risk: medium
impact: component
level: implementation
tags: [adapters, review-002, security, from-openapi]
---
## Description
Review 002 FWD-13 [major, security]. Lone `.`/`..` path-parameter
values survive the PATH_VALUE_ENCODE_SET (which has no `.`) and are
**silently normalized away by `Url::set_path`** — empirically
reproduced against the locked `url 2.5.8`:
- `set_path("/tenants/../admin")``/admin`
- `set_path("/files/..")``/`
- `set_path("/repos/%2e%2e/x")``/x` (so adding `%2e` to the encode
set would NOT fix it — the parser normalizes every case-insensitive
spelling)
Scenario: template `/tenants/{tenant}/resources` with peer input
`tenant = ".."` → the upstream receives `/resources` — a *different*
(east commonly less-scoped, list-everything) endpoint than the template
describes, **with the namespace's injected credentials attached**. The
existing dot-segment test (`traversal_value_cannot_escape_template_path`,
forward.rs:1155) only covers the multi-segment `../../admin` form the
encoder *does* catch. The origin check in `assemble_request_url`
(:520-522) cannot catch this — the origin never changes.
## Acceptance Criteria
- [ ] Rendered path **values** that are exactly `.` or `..`
(case-insensitive, including `%2e` spellings before encoding) are
rejected with a loud `INVALID_INPUT`-family CallError — the
cleanest point is `value_to_path_segment` / `render_path_template`
- [ ] A post-`set_path` invariant assert (decoded path segments =
base_dir + rendered segments, byte-identical) protects against
future normalizer surprises (implementer's choice: debug_assert
+ loud runtime check, or a property test with a dot/percent/binary
corpus)
- [ ] Tests: lone `..` value → error; lone `.` value → error; `%2e%2e`
spelling → error; a *legitimate* segment containing a dot
(`v1.2.3`, `.hidden-file` as a value) still works (the rejection
is exact-match, not substring)
- [ ] Existing traversal/encoding tests unchanged and passing
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part D', FWD-13)
- src/adapters/forward.rs:340-352 (encode set lacking `.`), :413-448 (renderer), :481-531 (set_path + origin check), :1155 (the test family)
- tasks/adapters/review-001-forward-url-safety.md (the FWD-01 fix this completes — one case short)
## Notes
The error message must not echo the raw value's neighbor inputs (no
spec/content leakage beyond the parameter name — follow the existing
message style in `render_path_template`'s unbound-placeholder error).
FWD-18 (object/array placeholder values double-routing to query) is
tracked separately in review-002-fwd17-19-contract-decisions — do not
fold it here, the mechanisms are adjacent but the decisions differ.
@@ -0,0 +1,70 @@
---
id: review-002-fwd16-missing-capability
name: Loud error on missing capability instead of silent unauthenticated request (FWD-16)
status: pending
depends_on: []
scope: narrow
risk: low
impact: component
level: implementation
tags: [adapters, review-002, security, from-openapi]
---
## Description
Review 002 FWD-16 [major]. The FWD-08 remediation made every
*malformed* credential fail loudly, but the **missing-capability arm
stays silent**: `forward.rs:200-238`
```rust
if let Some(scheme) = auth_scheme {
if let Some(secret) = context.capabilities.get(namespace) {
// ... four loud arms for malformed names/values
}
// capability == None → falls through: the request is sent
// with NO credential and NO diagnostic
}
```
`Capabilities::get` keys on `api_key:`/`http_token:` prefixes
(alkcall `core/types.rs:97-107`), so an assembly layer that registered
the secret under a wrong key form (or forgot) produces corrupted
upstream 401s at call time with zero local diagnostics. This is the
*most probable* misconfiguration shape and the one inconsistent arm in
an otherwise-loud match — every other arm was made loud by review-001's
FWD-08 fix (tested at forward.rs:1646-1736).
## Acceptance Criteria
- [ ] When `auth_scheme.is_some()` and the capability is absent, the
request is NOT sent; the caller gets an `INTERNAL`-family error
naming the missing capability key (e.g. "capability
`http_token:{namespace}` is absent; refusing to send the request
unauthenticated") — message contains no secret material
- [ ] When `auth_scheme.is_none()` the behavior is unchanged
(unauthenticated ops stay unauthenticated)
- [ ] Wire test: authed op, empty capabilities → error envelope (and
the test asserts via the existing `spawn_responder`-style seam
that the upstream received zero requests)
- [ ] Existing FWD-08 loud-arm tests unchanged and passing
- [ ] Doc note in the module doc: the loud-missing matrix now covers
malformed name/value AND absent capability
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part D', FWD-16)
- src/adapters/forward.rs:200-238 (the silent arm), :1646-1736 (the FWD-08 test family to mirror)
- docs/architecture/decisions/014-secret-material-flow-and-capability-injection.md
- tasks/adapters/review-001-forward-url-safety.md (FWD-08's loud-error work this completes)
## Notes
Error class choice: `INTERNAL` (misconfiguration, not caller fault) —
matches the FWD-08 precedent. Non-retryable, since re-sending cannot
succeed without the assembly layer changing. If a deployment legitimately
runs authed-ops with *optionally*-present credentials (none known
today), that would be a new config flag — do not preemptively add one;
the invariant (ADR-014: credentials flow only via Capabilities) argues
loud-and-closed.
@@ -0,0 +1,68 @@
---
id: review-002-fwd17-19-contract-decisions
name: Forwarding contract decisions — non-JSON SSE payloads, double-routed placeholders, percent preservation (FWD-17/18/19 decide+document or fix)
status: pending
depends_on: [review-002-fwd15-stream-timeout]
scope: narrow
risk: low
impact: component
level: implementation
tags: [adapters, review-002, from-openapi]
---
## Description
Three review-002 forwarding findings that are design decisions more
than bugs — decide, document, and fix where the decision says so:
- **FWD-17**: non-JSON SSE payloads silently degrade to JSON strings
(`forward.rs:774-781`, `unwrap_or(Value::String)`) and the parser's
`event:` field is discarded (`:977-979`). A consumer cannot extract
fields from a legitimately non-JSON stream, and an upstream's
SSE `event: error` convention is indistinguishable from data.
Decide: (a) document the JSON-or-string contract as-is, or (b) carry
the raw payload + the `event:` field name in the envelope for
non-JSON payloads (e.g. `{data, event}` object wrapper).
- **FWD-18**: non-scalar path-placeholder values double-route —
rendered into the path (as JSON text) *and* appended as a query
param (`forward.rs:144-157`). Fix the routing rule: a key that
matched a placeholder never also emits as query, regardless of value
shape (plus a spec-decision on whether object/array path values are
an error instead).
- **FWD-19 [info]**: preserved literal `%` in template/base text is
upstream-semantics-changing (`%2F` survives; most stacks route it
differently from `/`). Inputs are assembly-supplied (ADR-066 trust);
document the trade-off in the module doc (+ ADR-066 note) — or
reject raw `%` in *template text* while allowing it in values
(the strict shape).
## Acceptance Criteria
- [ ] A decision per finding, recorded (module doc + ADR-066 section
as appropriate); doc-only outcomes still need the doc change +
a doc-asserting test where applicable
- [ ] FWD-17: whichever shape is chosen, non-JSON payload behavior is
pinned by a test (string case, number-vs-string distinction,
event-field presence if option (b))
- [ ] FWD-18: placeholder keys never double-emit (query test with an
object value under a placeholder key) — and the object-in-path
outcome (error vs JSON-segment) decided and tested
- [ ] FWD-19: documented (and if the reject-raw-% shape is chosen, its
import- or call-time error tested)
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part D', FWD-17/18/19)
- src/adapters/forward.rs:774-781, :977-979, :144-157, :353-370, :481-505
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
- tasks/adapters/review-002-fwd13-dot-segments.md (adjacent template-renderer work — sequence or coordinate to avoid churn in the same helpers)
## Notes
Deliberately a decide-first task: none of the three is a crash or
leak; each is a contract the code half-implies. Keep the decisions
small and documented rather than building speculative machinery (e.g.
no new envelope schema unless (b) is actually chosen). Coordinate with
review-002-fwd13-dot-segments (same helpers, different concerns).
@@ -0,0 +1,79 @@
---
id: review-002-import-loudness-cluster
name: Import loudness cluster — oneOf body, self-ref requestBody, siblings, collisions, header precedence, error truncation (OAI-10/14/15/17/19, JS-02/03)
status: pending
depends_on: [review-002-oai11-ref-memoization]
scope: moderate
risk: low
impact: component
level: implementation
tags: [adapters, review-002, from-openapi, from-jsonschema]
---
## Description
Six review-002 minor findings across the two import files, all in the
"unsupported-feature loudness + import robustness" family (the OAI-06
matrix completion). Batched — each is small:
- **OAI-10**: `$ref` sibling keys are silently discarded
(openapi_spec.rs:396-410). Under 3.1 semantics siblings apply
alongside the `$ref` (advertise/enforce drift); there is no
`openapi: 3.1` version gate anywhere. Fix: warn on `$ref` siblings,
and/or gate `openapi: "3.1*"` like other unsupported features
(at minimum document the 3.0-only stance).
- **OAI-14**: top-level `oneOf` requestBodies (unconstraining `body`
contract — import-time gate), `discriminator`, `xml`, `callbacks`,
`security` blocks silently ignored. Extend the OAI-06 loudness
matrix: reject-or-warn at import with the established message
template.
- **OAI-15**: a self-`$ref`'d requestBody resolves to content-less →
body-less op registered silently (openapi_spec.rs:486-498). Fail
import with the OAI-04 error shape when a resolved requestBody
still contains a top-level `$ref` or lacks `content`.
- **OAI-17**: import errors echo unbounded spec-derived strings
(openapi_spec.rs:264-272 servers locations join; from_openapi.rs
placeholder/path interpolation). Truncate (first N + count) and cap
interpolated path/ref strings in `SchemaParse` messages.
- **OAI-19**: declared `in: header` params silently lose to
`default_headers` and credential headers (forward.rs:169-238 insert
order). Reject or warn at import when a header param collides with
a configured `default_headers` key or the auth scheme's header;
also reject peer-visible header params named `Authorization` on
authed namespaces.
- **JS-02**: `from_openapi` templates are not validated for balanced
braces at import (`/x{open` imports, fails per-call INTERNAL) —
reuse/hoist `from_jsonschema`'s `validate_path_template` into the
shared path and run it in `build_registration`.
- **JS-03**: `assert_eq!` in library code (`reject_collisions`,
from_openapi.rs:74-75) — return `Err(AdapterError::internal(…))` or
zip-iterate and drop the asserts.
## Acceptance Criteria
- [ ] Each item landed with its import-time test (loud-error content
asserted; sibling-warning case; header-collision warning/reject;
unterminated-template import failure; no asserts in non-test code)
- [ ] OAI-17: a 100k-path servers-list import error message is
bounded (test with a large synthetic spec asserting message
length < a sane cap)
- [ ] The OAI-06 matrix (or its successor doc section) lists the newly
loud features
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part E', OAI-10/14/15/17/19, JS-02, JS-03)
- src/adapters/openapi_spec.rs:396-410, :486-498, :241-273, :314-320, :264-272, :539-573
- src/adapters/from_openapi.rs:69-89 (JS-03), :162-171 (the "body" collision pattern to extend for OAI-19), from_jsonschema.rs (the template validator to hoist)
- tasks/adapters/review-001-openapi-loud-degradation.md (the OAI-06 matrix), tasks/adapters/review-001-openapi-import-integrity.md
## Notes
Sequence after review-002-oai11-ref-memoization (same resolver
function for OAI-10/15 — or rebase-verify). Slice per finding, commit
per finding. The OAI-19 import-time check may not see the assembly
layer's `default_headers` (config lives elsewhere) — if the
import-time surface cannot know, the loud point moves to
first-call-time (warn-once) — implementer documents which.
@@ -0,0 +1,56 @@
---
id: review-002-js01-placeholder-check
name: Fix placeholder check skipped when input_schema has no properties (JS-01)
status: pending
depends_on: []
scope: single
risk: low
impact: component
level: implementation
tags: [adapters, review-002, from-jsonschema]
---
## Description
Review 002 JS-01 [major]. `from_jsonschema`'s eager placeholder-binding
check is skipped when `input_schema` has no `properties` key:
`spec_name_references_undeclared` (`from_jsonschema.rs:166-188`) hits
`if properties.is_empty() { return false; }` at :173-175. So:
```rust
FromJsonSchema::new(spec_with_input_schema_type_object_no_properties,
path_template = "/widgets/{id}")
```
**passes construction** — then every call fails (`{id}` unbound →
INTERNAL at forward.rs:441-446; a peer-supplied `id` key →
INVALID_INPUT). The from_openapi equivalent (`unbound_placeholders`,
from_openapi.rs:36-57) handles the empty case correctly: with no
declared properties, any placeholder is unbound. The existing test
(from_jsonschema.rs:699-724) covers only the properties-present case.
## Acceptance Criteria
- [ ] Drop the `is_empty` early return (or return `true` for
placeholders when properties are empty — same semantics as the
from_openapi path)
- [ ] Test: `FromJsonSchema::new` with a no-properties schema +
`{id}` template fails at construction with the placeholder
error; a no-properties schema with a placeholder-free template
still constructs fine
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part E', cluster item JS-01; Test-gap 11)
- src/adapters/from_jsonschema.rs:166-188 (the early return), :77-93 (the construction-time validation), :699-724 (the present-props test)
- src/adapters/from_openapi.rs:36-57 (the correct reference implementation)
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
## Notes
One-line fix + one test. Explicitly approved to fold into any commit
that is already touching from_jsonschema.rs (e.g. the wire-tests task)
if a session prefers — but if touched standalone, keep it its own
commit per the small-units convention.
@@ -0,0 +1,46 @@
---
id: review-002-mcp-batch-cap
name: Cap MCP batch tool at MAX_BATCH_OPERATIONS (PRJ-22)
status: pending
depends_on: []
scope: single
risk: low
impact: component
level: implementation
tags: [adapters, review-002, mcp]
---
## Description
Review 002 PRJ-22 [minor]. The MCP `batch` tool has no cap on the
`calls` array (`to_mcp.rs:215-247` executes serially) while HTTP
`/batch` rejects at 101 (`routes.rs:173-184`,
`MAX_BATCH_OPERATIONS`). A 10,000-entry MCP batch occupies the
dispatch spine serially (each invoke up to the 30 s deadline) — the
doc (PRJ-09's fix) advertises no limit either.
## Acceptance Criteria
- [ ] The `batch` tool enforces the same `MAX_BATCH_OPERATIONS` cap
(share the constant; do not duplicate the literal) — over-cap →
structured `INVALID_INPUT`-family error consistent with the
tool's other argument errors (including `retryable`, per PRJ-13)
- [ ] Tool description states the limit (mirroring how `/batch` docs
it) — ADR-041 shape unchanged (4 fixed tools)
- [ ] Test: over-cap `calls` array → structured error, no dispatch
happened (spy/gateway-count assert via the existing invoke_tool
harness)
- [ ] `cargo test --features mcp`, `cargo clippy --all-features
--all-targets -- -D warnings`, `cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part F', PRJ-22)
- src/adapters/to_mcp.rs:215-247 (the loop), src/gateway/routes.rs:42,173-184 (the cap to share)
- tasks/adapters/review-001-mcp-tool-fidelity.md (the PRJ-07..10/13 work whose conventions to follow)
## Notes
One of the smallest tasks in the batch — a good first warm-up slice or
fold-in candidate if a session runs short (but keep the commit
separate; it is its own unit of work).
@@ -0,0 +1,70 @@
---
id: review-002-oai11-ref-memoization
name: Memoize $ref expansion to bound resolver work (OAI-11)
status: pending
depends_on: []
scope: moderate
risk: medium
impact: component
level: implementation
tags: [adapters, review-002, from-openapi, openapi-spec]
---
## Description
Review 002 OAI-11 [major, borderline critical]. The OAI-01 fix
(cycle guard + depth budget, `openapi_spec.rs:382-437`) bounds *stack*,
not *work*: every `$ref` hop re-expands its target from a fresh clone
with no memoization. A chain of shared but acyclic refs —
```
S_i = { a: { $ref: "S_{i+1}" }, b: { $ref: "S_{i+1}" } }
```
(the doubly-linked-list `prev`/`next` shape — valid, common) expands as
a full binary tree. Empirically (verbatim harness): 20 levels → 7.3M
node visits / 3.6 s; growth ≈ 2^levels → 30 levels ≈ ~1 hour, 40 ≈ days.
Neither guard fires (`err=false`); the import just wedges — no stack
overflow, no error. The same "valid document kills the process" class
as OAI-01, one mitigation shy.
## Acceptance Criteria
- [ ] `resolve_refs_bounded` memoizes resolved, cycle-free `$ref`
targets per document (cache keyed on the ref string; clone the
resolved `Value` on hit instead of re-resolving) — or an
equivalent total-node-count budget with a clean
`SchemaParse`-family error (implementer's choice; memoization
strongly preferred — it preserves full acyclic support)
- [ ] The cycle guard and both depth checks remain (memoization must
not alter cycle detection semantics — a cycle still errors)
- [ ] Acceptance gate: a 30+ level shared-chain spec imports in
bounded time (add the test with a wall-clock or node-count
assert; a 30-level chain must import in < 1 s)
- [ ] Tests: diamond (non-cyclic shared refs) unchanged-correct;
cycle-through-shared-node still errors; memoized result equals
the old expansion for a shared-ref schema (golden compare on one
case)
- [ ] The four missing cycle-shape tests from OAI-16 (array-items,
allOf, additionalProperties, `$ref`-sibling) land here too —
same function, same risk envelope
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part E', OAI-11 + OAI-16)
- src/adapters/openapi_spec.rs:382-437 (the resolver), :713-805 (existing cycle tests), from_openapi.rs:736 (the requestBody-chain test)
- tasks/adapters/review-001-ref-cycle-guard.md (the OAI-01 fix this completes)
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
## Notes
Clone cost per hit is fine (schemas are small; the win is avoiding
exponential re-resolution, not allocation). If memoizing, note the
`resolving`-set interaction: a memo entry must only be written for
*completed* (cycle-free) expansions — a partial expansion under an
active cycle attempt must never be cached (write-on-success at the
top of the recursion return path, or post-order). This is the one
subtle part; the existing cycle tests + the four new shape tests are
the safety net.
@@ -0,0 +1,70 @@
---
id: review-002-oai13-path-item-wildcards
name: Path-item parameters, 2XX/4XX/5XX wildcards, webhooks handling (OAI-13)
status: pending
depends_on: [review-002-oai11-ref-memoization]
scope: moderate
risk: medium
impact: component
level: implementation
tags: [adapters, review-002, from-openapi, openapi-spec]
---
## Description
Review 002 OAI-13 [major]. Three common real-world OpenAPI shapes are
still silently mishandled (verified at `openapi_spec.rs:276-336` +
`from_openapi.rs:136-145`):
1. **Path-item-level `parameters`** (shared params declared next to
the path key — extremely common in real specs) never merge into
operations → `{id}` unbound → the whole import fails with a
*misleading* diagnosis (the skip filter at :314-320 explicitly
whitelists `"parameters"` out of the unsupported-methods warning,
so nothing names the cause).
2. **Response wildcard keys**: `"2XX"` is missed by the SSE-detection
success sweep (an SSE stream under `2XX` imports as a giant-text
Mutation — the exact OAI-06 misbehavior, one spelling away);
`"4XX"/"5XX"` error keys drop silently with the generic warn (the
test at from_openapi.rs:840 enshrines silent `5XX` dropping).
3. **Top-level `webhooks`** silently vanish from mixed documents.
## Acceptance Criteria
- [ ] Path-item `parameters` merge into each operation under the path
(operation-level entries override per OpenAPI spec semantics);
the import no longer fails with "unbound placeholder" for
shared-param specs
- [ ] Success sweep accepts `"2XX"` (SSE detection + output schema)
and error sweep accepts `"4XX"/"5XX"` (`HTTP_`-class mapping per
the wildcard's implied status range — choose the mapping, e.g.
4XX → the strongest available code, and document it)
- [ ] `webhooks`: import (as Mutations/Webhook-kind ops or a decided
visibility) or reject the key loudly — pick per ADR-066's
single-endpoint adapter philosophy and record the decision
- [ ] The misleading-diagnosis fix: when import fails after skipping
path-item parameters, the error names the actual cause (the
skip-filter whitelist must not hide the parameter key from the
unsupported-features warning)
- [ ] Tests: shared-path-params spec imports correctly (incl.
operation-override precedence); `2XX`-declared SSE imports as
Sub; `5XX`-declared error lands in error schemas; webhooks-only
and mixed docs behave per the decision
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part E', OAI-13; Test-gaps: Petstore-with-shared-params)
- src/adapters/openapi_spec.rs:276-336 (method sweep + skip filter), from_openapi.rs:131-151 (success sweep), :248-270 (error sweep), :840 (the enshrining test)
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md, /051-yaml-input-for-from-openapi.md
- tasks/adapters/review-001-openapi-loud-degradation.md (the OAI-06 matrix this extends)
## Notes
Slice suggestion: (1) path-item parameters + diagnosis fix, (2)
wildcard sweeps, (3) webhooks decision. The wildcard-to-error-status
mapping is the one judgment call worth a line in ADR-066 (e.g. "4XX
wildcards project to their range's first legal HTTP_ code per error
dedup rules"). Sequence after review-002-oai11-ref-memoization (same
file, resolver shape changes first) — or rebase-verify.
@@ -0,0 +1,88 @@
---
id: review-002-projection-truthfulness
name: Projection doc truthfulness — dangling CallError ref, status clobber/drifts, OAS-invalid extension (PRJ-16b/17/18/19/20/21/23/24)
status: pending
depends_on: []
scope: moderate
risk: low
impact: component
level: implementation
tags: [adapters, review-002, to-openapi]
---
## Description
Eight review-002 findings in `to_openapi.rs` (+ one in `to_mcp.rs`),
all "the generated document must be runtime-truthful and
OAS-valid" — the PRJ-01..15 remediation's remaining tail:
- **PRJ-16b [major]**: `BatchResultEntry.error` refs
`#/components/schemas/CallError` which does not exist
(to_openapi.rs:602 vs components at :387-413 defines `CallFailure` +
per-code variants). The openapiv3 parse test runs on an **empty
registry** so nothing trips. Generated clients/validators fail to
resolve.
- **PRJ-17**: an op declaring `HTTP_404@404` clobbers the shared
NotFound response on `/call` (:694/:237-239/:295-300) — losing
`NOT_FOUND` which the runtime still emits; every real 404 then
violates the documented schema. Merge op codes into the shared
response (oneOf append) instead of overwriting.
- **PRJ-18**: `/publish` 400 over-declares `INVALID_OPERATION_TYPE`;
runtime maps that condition to 401 without a token (the 401 entry in
the same doc is the true one) (:268-277).
- **PRJ-19**: extractor plain-text 415 (missing content-type) and
data-error 422 (JsonDataError) undocumented on `/call`, `/batch`,
`/subscribe` (axum 0.8 emits them; the module doc admits
"plain-text 400/415/422") (:673-682).
- **PRJ-20**: `/call` 401 under-declares — unauthenticated Sub/Pub
call → 401 with `INVALID_OPERATION_TYPE`, absent from the enum
(:218-219 vs error.rs:67-73; `/publish` got this split right).
- **PRJ-21**: `/batch` documents a 500 the runtime never emits (all
dispatch failures are in-band entries) (:329-334).
- **PRJ-23**: `x-operation-error-statuses` is emitted *inside*
`components.schemas` (:414-417) — extension keys are legal on
`components`, not as a schema name; OAS-invalid on any registry with
op errors. Move to `components` level or delete (nothing consumes
it).
- **PRJ-24**: dead `search_filter` parameter in `to_mcp::handle_batch`
(computed then `let _ =` discarded) — remove.
## Acceptance Criteria
- [ ] PRJ-16b: `CallError` defined (generic code/message/retryable/
details) OR `error` points at existing per-code components via
oneOf; the openapiv3 parse test runs on a **populated** registry
(with op errors — exercising PRJ-23's key too)
- [ ] PRJ-17: protocol-status op errors merge into the shared response
(oneOf append); test with `HTTP_404@404` asserting `NOT_FOUND`
survives alongside
- [ ] PRJ-18/20: 400 and 401 oneOf lists match error.rs's actual
mapping (drop IOT from publish-400; add IOT to call-401) —
conformance tests for both bodies
- [ ] PRJ-19: 415 + plain-text 422 slots documented (extend
`plain_text_extractor_rejection` or an equivalent shared
component)
- [ ] PRJ-21: unreachable /batch 500 removed or marked reserved
- [ ] PRJ-23: extension key legal placement (or deleted); parse test
covers the with-errors path
- [ ] PRJ-24: dead parameter removed from `handle_batch`
- [ ] Golden-doc tests updated (byte-identical assertion refreshed);
`cargo test --all-features`, `cargo clippy --all-features
--all-targets -- -D warnings`, `cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part F', PRJ-16b/17/18/19/20/21/23/24; Test-gaps 5/6)
- src/adapters/to_openapi.rs:387-433 (components), :602 (dangling ref), :218-300 (status oneOfs), :414-417 (extension), :673-682 (extractor text)
- src/gateway/error.rs:57-81 (the runtime mapping these must match), routes.rs (the runtime truth)
- tasks/adapters/review-001-openapi-projection-fidelity.md (the PRJ-01..15 work this completes)
## Notes
The review-001 lesson applies again: every fix here needs its test to
run against a populated registry (the empty-registry parse test is how
PRJ-16b and PRJ-23 escaped). Consider one shared
`golden_doc_with_ops()` fixture replacing per-test registry setup.
ADR-045's version bump: these are doc-contract corrections — bump
`info.version` per ADR-045's tracking rule (gateway endpoint contract
itself unchanged).
@@ -0,0 +1,69 @@
---
id: review-002-yaml-normalization
name: YAML input normalization — duplicates, .inf, merge keys, non-string keys (OAI-12)
status: pending
depends_on: []
scope: narrow
risk: medium
impact: component
level: implementation
tags: [adapters, review-002, from-openapi, openapi-spec]
---
## Description
Review 002 OAI-12 [major]. `OpenAPISpec::from_yaml`
(`openapi_spec.rs:153-158`) is a single `yaml_serde::from_str::<Value>`
with zero post-parse normalization; three verified corruptions flow
through unimpeded (empirically confirmed against yaml_serde 0.10.7):
1. **Duplicate keys silently last-win** on the Value path
(`visit_map → values.insert`) while the JSON path *errors* on the
same document — the two input formats disagree, YAML's failure
mode is silent.
2. **`.inf`/`.nan` numbers become `Value::Null`** through
`Number::from_f64(∞) → None` — a declared constraint (`maximum:
.inf` is unusual, but any float inf) silently vanishes from the
advertised schema.
3. **Merge keys (`<<: *anchor`) are not applied** (yaml_serde's
`apply_merge()` is opt-in and unused) — `<<` survives as a literal
property name in the resolved/advertised schema.
Non-string keys (`200:` response codes as numbers etc.) were checked —
plain statuses survive via string round-trips — but numeric inference
into any string-matched field is a live hazard (e.g. `style: 1`).
## Acceptance Criteria
- [ ] A post-parse normalization pass on the YAML path (before
`from_value`): reject duplicate keys (loud, naming the path or
at least the document context), reject `.inf/.nan`-derived
nulls-from-floats (either reject or reject-with-context), and
decide merge keys: either invoke `apply_merge()` (matching YAML
1.1 user expectations; ADR-051 declares YAML 1.2 via yaml_serde —
verify which the crate claims) or reject `<<` keys loudly
- [ ] Non-string keys: reject or coerce-with-loudness (decide; JSON
path parity is the goal — the same document should mean the same
thing through both entry points or the difference must be loud)
- [ ] Tests at the `from_yaml` seam (not the parser): duplicate keys →
error; `.inf` → loud error (not silent null); merge key applied
or rejected; quoted-always spec (`"200":`) unchanged
- [ ] A doc note on the YAML/JSON parity contract (ADR-051 section or
the module doc)
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
- docs/reviews/002-post-remediation-review.md (Part E', OAI-12; Test-gap 9)
- src/adapters/openapi_spec.rs:153-158 (the seam), from_openapi.rs (the consumers)
- docs/architecture/decisions/051-yaml-input-for-from-openapi.md
- tasks/adapters/review-001-openapi-import-integrity.md (the import-integrity context)
## Notes
Keep the walk cheap (one pass, no allocation beyond error messages).
The alias-bomb/depth bounds are dependency-provided and verified
working (review 002 re-verified) — do not re-implement them; the task
is about *semantic* normalization, not resource limits. ADR-051 is the
decision record to update if the merge-key stance changes behavior.