Skip to content

feat(drafts): Cycles → APS Tier-1 denial reason mapping (v0.1 draft)#93

Merged
amavashev merged 6 commits into
mainfrom
feat/cycles-aps-denial-mapping-v0.1-draft
May 13, 2026
Merged

feat(drafts): Cycles → APS Tier-1 denial reason mapping (v0.1 draft)#93
amavashev merged 6 commits into
mainfrom
feat/cycles-aps-denial-mapping-v0.1-draft

Conversation

@amavashev
Copy link
Copy Markdown
Contributor

Summary

New draft at drafts/cycles-aps-denial-mapping-v0.1.md specifying the contract for mapCyclesDenialToFoundation() — the function the APS-side Cycles adapter (src/v2/payment-rails/cycles/index.ts in aeoess/agent-passport-system) will implement to translate Cycles denial signals into APS PaymentReceipt Tier-1 denial_reason values.

Sister artifact to #90 (CyclesEvidence v0.1). #90 defines how Cycles denials get signed and content-addressed; this draft defines how those denials are projected into the APS PaymentReceipt's closed 6-value Tier-1 enum, with the richer Cycles-specific detail preserved Tier-2 under cycles.denial_detail.

What's in scope (v0.1)

  • Two mapping tables with per-row compression notes:
    • Cycles ErrorCode → APS Tier-1 (15 closed values from canonical L429-L446)
    • Cycles DecisionReasonCode → APS Tier-1 (9 known values, open enum, from canonical L487-L545)
  • Unknown-value handling rule — the open DecisionReasonCode enum requires graceful degrade per canonical L503-L505
  • Tier-2 namespace speccycles.denial_detail schema for both ErrorCode-sourced and DecisionReasonCode-sourced denials
  • Forward-compat noterequires_owner_confirmation has no Cycles source; documented for adapter authors
  • TypeScript reference implementation as a code block, directly droppable into src/v2/payment-rails/cycles/index.ts
  • Golden test cases mapping to the three denial-path fixtures already in drafts/fixtures/cycles-evidence-v0.1/cases/ (03, 11, 12), proving the round-trip works against real signed envelopes

Why now

Lands the mapping contract so it's review-ready when the APS-side SDK PR adding rail.budget_reservation.{permit,release,denial}.v1 literals opens. The adapter PR (the function's actual TypeScript home) is blocked on that SDK PR landing; this draft is what unblocks the adapter review when it's submitted.

Per-row compression notes are required per aeoess/agent-passport-system#25 comment 4422627045 ("where several Cycles reasons collapse into spend_limit_exceeded, want a one-line note per row in the mapping table so future readers know the compression is intentional and lossy"). Every row carries one.

Rigor pass

All verified before commit per the feedback_spec_review_rigor protocol established on #90:

  • All 15 ErrorCode values present and match canonical enum; none invented
  • All 9 DecisionReasonCode values present and match canonical description's "KNOWN VALUES" sections
  • All APS Tier-1 values used are within the closed 6-value enum; the unused 6th value (requires_owner_confirmation) is documented with rationale
  • All 13 canonical line citations spot-checked against the exact yaml text on each cited line
  • All 4 referenced aeoess/agent-passport-system#25 comment IDs verified to exist on the issue via gh api
  • TypeScript impl const tables match the markdown tables row-for-row (15 + 9 entries)

Status

DRAFT (v0.1). Will move to a numbered spec file at repo root once:

  1. The APS-side adapter ships with this mapping implemented
  2. The APS-side SDK PR adding rail.budget_reservation.*.v1 literals has merged
  3. End-to-end test coverage demonstrates the round-trip (Cycles denial → CyclesEvidence → APS PaymentReceipt with intact Tier-2 detail)

References

  • runcycles/cycles-protocol#90 (CyclesEvidence v0.1 envelope — sister artifact)
  • aeoess/agent-passport-system#25 (APS integration thread — comments 4413731054, 4422182941, 4422627045, 4433715146 directly cited)
  • aeoess/agent-governance-vocabulary#92 (Cycles signal-type crosswalk — open)
  • cycles-protocol-v0.yaml §ErrorCode, §DecisionReasonCode

Test plan

  • Review confirms the two mapping tables are complete (15 ErrorCode + 9 DecisionReasonCode rows) and the Tier-1 targets are all within the closed APS enum
  • Review confirms per-row compression notes are present on every lossy row (no silent collapses)
  • Review confirms the TypeScript reference impl matches the markdown tables byte-for-byte
  • (After APS SDK PR merges) Adapter PR drops the TS impl into src/v2/payment-rails/cycles/index.ts and adds unit tests against the three golden fixtures (03, 11, 12)
  • (After adapter PR lands) Promote this doc out of drafts/ to a numbered spec file at repo root

amavashev added 2 commits May 13, 2026 07:30
New draft at `drafts/cycles-aps-denial-mapping-v0.1.md` specifying the
contract for `mapCyclesDenialToFoundation()` — the function the
APS-side Cycles adapter (`src/v2/payment-rails/cycles/index.ts` in
`aeoess/agent-passport-system`) will implement to translate Cycles
denial signals into APS PaymentReceipt Tier-1 `denial_reason` values.

Why now:

Sister artifact to `drafts/cycles-evidence-v0.1.yaml` (merged #90).
The CyclesEvidence envelope defines how Cycles denials are signed and
content-addressed; this doc defines how those denials are projected
into the APS PaymentReceipt's closed 6-value Tier-1 enum, with the
richer Cycles-specific detail preserved Tier-2 under `cycles.denial_detail`.

Lands now so the mapping is review-ready when the APS-side SDK PR
adding `rail.budget_reservation.{permit,release,denial}.v1` literals
opens. The adapter PR (the function's actual home in TypeScript) is
blocked on that SDK PR landing; this draft is what unblocks the
adapter review when it's submitted.

What's in scope (v0.1):

  - Two mapping tables with per-row compression notes:
    - Cycles `ErrorCode` → APS Tier-1 (15 closed values from canonical
      L429-L446)
    - Cycles `DecisionReasonCode` → APS Tier-1 (9 known values, open
      enum, from canonical L487-L545)
  - Unknown-value handling rule (the open `DecisionReasonCode` enum
    requires graceful degrade per canonical L503-L505).
  - Tier-2 `cycles.denial_detail` namespace spec — what fields ride
    under it for ErrorCode-sourced vs DecisionReasonCode-sourced
    denials.
  - Documentation of the one APS Tier-1 value with no Cycles source
    (`requires_owner_confirmation` — Cycles has no analog).
  - TypeScript reference implementation as a code block, directly
    droppable into `src/v2/payment-rails/cycles/index.ts`.
  - Golden test-case mapping to the three denial-path fixtures already
    committed at `drafts/fixtures/cycles-evidence-v0.1/cases/` (03,
    11, 12), proving the round-trip works against real signed envelopes.

Per-row compression notes are required per the integration thread
(`aeoess/agent-passport-system#25` comment 4422627045 — "where several
Cycles reasons collapse into spend_limit_exceeded, want a one-line
note per row in the mapping table so future readers know the
compression is intentional and lossy"). Every row carries one.

Rigor pass (per `feedback_spec_review_rigor` memory rule, all
verified before commit):

  - All 15 `ErrorCode` values present and match canonical enum;
    none invented.
  - All 9 `DecisionReasonCode` values present and match canonical
    description's "KNOWN VALUES" sections.
  - All APS Tier-1 values used are within the closed 6-value enum
    (`no_commerce_scope` / `spend_limit_exceeded` / `wallet_revoked` /
    `time_window_violation` / `rail_error` /
    `requires_owner_confirmation`); the unused 6th value
    (`requires_owner_confirmation`) is documented with rationale.
  - All 13 canonical line citations spot-checked against the exact
    yaml text on each cited line.
  - All 4 referenced `aeoess/agent-passport-system#25` comment IDs
    verified to exist on the issue via `gh api`.
  - TypeScript impl const tables match the markdown tables row-for-row
    (15 ErrorCode entries + 9 DecisionReasonCode entries).

Status DRAFT (v0.1). Will move to a numbered spec file at repo root
once the APS-side adapter ships with this mapping implemented, the
SDK PR adding the `rail.budget_reservation.*.v1` literals has merged,
and end-to-end test coverage demonstrates the Cycles denial →
CyclesEvidence → APS PaymentReceipt round-trip with intact Tier-2
detail.

References:

  - `#90` (CyclesEvidence v0.1 envelope —
    sister artifact)
  - `aeoess/agent-passport-system#25` (the APS integration thread —
    comments 4413731054, 4422182941, 4422627045, 4433715146 directly
    cited)
  - `aeoess/agent-governance-vocabulary#92` (Cycles signal-type
    crosswalk — open)
  - `cycles-protocol-v0.yaml` §ErrorCode, §DecisionReasonCode (the
    canonical sources for the mapped enums)
Self-audit caught two unverified claims I'd asserted without checking
against `aeoess/agent-passport-system`. Both now verified and fixed.

1. Wrong line citation: `hooks.ts:115-123` was the cited location of
   the denial validation logic. Actual locations in the current
   `aeoess/agent-passport-system/src/v2/payment-rails/hooks.ts`:
     - `VALID_DENIAL_REASONS` array definition: L87-L94
     - `emitDenial` rejection: L381
     - Verifier rejection paths: L606, L629
   Replaced the single wrong citation with all four correct locations.

2. Made-up type name: the TypeScript reference impl imported
   `TierOneDenial` (which doesn't exist in APS). The actual exported
   type from `src/v2/payment-rails/types.ts` is `DenialReason` (a
   closed string-literal union of the six values). Replaced
   `TierOneDenial['denial_reason']` with `DenialReason` directly in
   both const-table types and the function signature. Added an
   explicit local `FoundationDenialMapping` interface for the return
   shape with documented field types — so adapter authors can see the
   full Tier-1 + Tier-2 contract at a glance.

Other audit findings (verified, no fix needed):
  - All 15 ErrorCode values match canonical enum byte-for-byte
  - All 9 DecisionReasonCode values match canonical KNOWN VALUES
  - All 6 Tier-1 values verified against APS `VALID_DENIAL_REASONS`
    array (`hooks.ts:87-94`) AND `DenialReason` type union
    (`types.ts`)
  - The `src/v2/payment-rails/cycles/index.ts` path is correctly
    future-anticipated (current rails are acp/ap2/mpp/stripe-issuing/
    x402; cycles/ will be created by the adapter PR per issue #25
    comment 4422627045)
  - All three golden-fixture test predictions match the actual
    fixture contents:
      03-reserve-dry-run-deny: reason_code=BUDGET_EXCEEDED
        → spend_limit_exceeded ✓
      11-reserve-live-budget-exceeded: ErrorCode=BUDGET_EXCEEDED
        → spend_limit_exceeded ✓
      12-decide-live-forbidden: ErrorCode=FORBIDDEN
        → no_commerce_scope ✓
  - The v0.1.26 extension references (denied_action_kinds,
    allowed_action_kinds, DenyDetail) all exist in
    cycles-protocol-extensions-v0.1.26.yaml as cited

Process note (per the `feedback_spec_review_rigor` memory rule):
this is exactly the kind of citation drift the protocol exists to
catch. The original `hooks.ts:115-123` claim came from a
memory-of-an-earlier-conversation about the APS source — I had not
re-verified it against the live repo before writing the doc. Going
forward: every line citation gets a `gh api` fetch + grep before
commit, not after.
@amavashev
Copy link
Copy Markdown
Contributor Author

Self-audit pass before requesting review — found 2 unverified claims in the original commit, fixed in 78803dc. Documenting in the open so reviewers see the trail.

What I'd skipped verifying

I'd written the doc citing hooks.ts:115-123 and importing TierOneDenial from APS — both based on memory of earlier conversations with aeoess, not on the live agent-passport-system source. Per my own rigor protocol, every cross-repo line citation and type name should be fetched-and-grepped, not recalled.

What the audit found

Wrong line citation. hooks.ts:115-123 is inside _verifyOwnerConfirmationAgainstView, not the denial validation. Real locations in current aeoess/agent-passport-system/src/v2/payment-rails/hooks.ts:

  • VALID_DENIAL_REASONS array definition: L87-L94
  • emitDenial rejection: L381
  • Verifier rejection paths: L606, L629

Replaced the single wrong citation with all four correct locations. The "closed taxonomy enforced on three axes" framing is now visible to adapter readers (TS union + runtime array + three call sites that throw INVALID_DENIAL_REASON).

Made-up type name. The TS reference impl imported TierOneDenial (doesn't exist). The actual exported APS type is DenialReason (a closed string-literal union in types.ts). Replaced with the real type, and added an explicit FoundationDenialMapping interface for the function's return shape so adapter authors see the full Tier-1 + Tier-2 contract at a glance.

What was already correct (verified, no fix needed)

  • All 15 ErrorCode values match canonical enum byte-for-byte
  • All 9 DecisionReasonCode values match canonical KNOWN VALUES sections
  • All 6 Tier-1 values verified against APS VALID_DENIAL_REASONS array AND DenialReason type union — exact match in exact order
  • src/v2/payment-rails/cycles/index.ts is correctly future-anticipated (the directory doesn't exist yet; current rails are acp/ap2/mpp/stripe-issuing/x402; per issue spec: broaden UNIT_MISMATCH to cover reserve; document 404 on reserve + event #25 comment 4422627045, cycles/ is the agreed home)
  • All 3 golden-fixture predictions match actual fixture contents (verified by reading the signed JSON)
  • v0.1.26 extension references (denied_action_kinds, allowed_action_kinds, DenyDetail) all exist as cited

Process note

This is exactly the citation-drift class my feedback_spec_review_rigor memory rule exists to catch — and the only reason it wasn't caught pre-commit is that I trusted memory over re-verification. Adjusting going forward: every cross-repo line citation gets a gh api fetch + grep before the commit lands, not after the self-audit.

Ready for review.

amavashev added 2 commits May 13, 2026 07:56
Reviewer's first pass found 4 real issues. All empirically verified
against the canonical sources before fixing.

1. ErrorCode table was missing the 3 v0.1.26 extension codes.

`cycles-protocol-extensions-v0.1.26.yaml` L516-L543 declares
ACTION_QUOTA_EXCEEDED, ACTION_KIND_NOT_ALLOWED, and ACTION_KIND_DENIED
as MUST-add additions to the ErrorCode enum. The doc had them only on
the DecisionReasonCode side (mapping table 2) — so the same conditions
mapped to spend_limit_exceeded / no_commerce_scope via DecisionReasonCode
but fell back to rail_error via ErrorCode. Now consistent across both
paths: same Tier-1 mapping regardless of whether the denial surfaces
on a 2xx (decide / dry-run) or 4xx (live non-dry) wire path.

Knock-on issue documented but not fixed in this commit: the merged
`drafts/cycles-evidence-v0.1.yaml` `ErrorResponseMirror.error` enum is
fixed to the 15 v0.1.25 base values. A v0.1.26 server emitting an
extension ErrorCode would fail v0.1 evidence schema validation. The
doc adds a "v0.1 evidence-schema dependency" note explaining this and
treats the 3 extension rows as forward-compat declarations. A follow-up
PR against cycles-protocol main will extend the schema enum.

2. v0.1.26 DenyDetail preservation was promised but not delivered.

The previous draft said action-kind denials preserve DenyDetail Tier-2
under `cycles.denial_detail.deny_detail`, but the TS reference impl
didn't populate it and the merged CyclesEvidence v0.1 response mirrors
are `additionalProperties: false` without a `deny_detail` field — so
the promise was empty.

Scoped DenyDetail OUT of v0.1 with an explicit limitation note. v0.1
preserves only the raw `code` Tier-2; v0.2 promotion requires
amending the evidence schema, fixture set, and reference impl to carry
the structured DenyDetail through. The trade-off is documented: v0.1
adapters get correct Tier-1 compression and a recoverable canonical
denial identifier, but lose the rich scope/quota/policy detail that
v0.1.26 carries on the wire.

3. Inconsistent Tier-2 code field name (used "error", "reason_code",
   and "code" interchangeably).

Standardized on `code` everywhere. The "source" discriminator
(`"ErrorCode"` vs `"DecisionReasonCode"`) still indicates where the
code originated, but the field name is `code` regardless.

4. JSON example used TypeScript union syntax (`"ErrorCode" |
   "DecisionReasonCode"`), making it invalid JSON.

Replaced the union with the concrete value `"ErrorCode"` (since the
example is the BUDGET_EXCEEDED / 409 case anyway), and added a prose
paragraph below explaining the two source values and the
context-dependent presence of `http_status` / `message` / `request_id`.

Validation:

  - JSON example parses cleanly (json.loads succeeds)
  - 0 stray `cycles.denial_detail.error` or `cycles.denial_detail.reason_code`
    references — all standardized to `.code`
  - 18 ErrorCode rows in mapping table 1 (15 base + 3 v0.1.26 extension)
  - 18 entries in TS ERROR_CODE_TO_TIER1 const (matches table)
  - Extension citation L516-543 verified against actual canonical lines

Follow-up: separate PR will extend `ErrorResponseMirror.error` enum in
`drafts/cycles-evidence-v0.1.yaml` to include the 3 v0.1.26 codes,
add a fixture exercising one of them, and unblock the forward-compat
declarations in this doc.
…26 additions)

User flagged that cycles-protocol-extensions-v0.1.26 has not been
implemented in any Cycles deployment yet. The prior commit (89c1aeb)
added v0.1.26 ErrorCode rows to mapping table 1 and TS const for
cross-path consistency with the v0.1.26 DecisionReasonCode rows that
were already in table 2 — but the right resolution is the inverse:
remove v0.1.26 from BOTH tables until the extension actually ships
in a deployment.

What the reviewer was asking for vs. what was needed:

  - Reviewer's framing (spec-text as ground truth): v0.1.26 codes
    appear in both ErrorCode and DecisionReasonCode canonical enums,
    so the mapping doc should map them on both sides for consistency.
  - User's correction (deployment reality as ground truth): the
    extension spec exists in the repo but no Cycles server emits
    those codes. Documenting their mapping is premature — no
    deployment exercises the contract and no fixture can verify
    against it.

Resolution: scope this v0.1 doc to v0.1.25 base protocol surface
only. Mapping table 1 back to 15 ErrorCodes; mapping table 2 back to
6 DecisionReasonCodes; TS const tables match. The DenyDetail
preservation question (previously F2) collapses into the same scope-
out — v0.1.26 entirely is the v0.2 promotion criterion, not just
DenyDetail.

Replaced the previous "v0.1 evidence-schema dependency note" and
"v0.1.26 DenyDetail is out of scope" sections with a single
consolidated "v0.1.26 extension is out of scope for v0.1" section
explaining:

  - The extension defines three ErrorCodes / DecisionReasonCodes plus
    DenyDetail, all unimplemented.
  - The sister evidence schema enum (ErrorResponseMirror.error) is
    correspondingly fixed to the 15 v0.1.25 base values.
  - v0.2 promotion criterion lists the four steps that must happen
    when a deployment ships v0.1.26: add three rows to each mapping
    table with cross-path-consistent Tier-1 mappings; add a
    deny_detail field to the evidence schema; add at least one fixture.

Validation:

  - 15 ErrorCode rows in mapping table 1 (was 18, now 15)
  - 6 DecisionReasonCode rows in mapping table 2 (was 9, now 6)
  - TS ERROR_CODE_TO_TIER1: 15 entries (was 18, now 15)
  - TS DECISION_REASON_TO_TIER1: 6 entries (was 9, now 6)
  - 0 v0.1.26 extension codes referenced in either table or TS const
  - JSON example still parses cleanly
  - Stray cycles.denial_detail.{error,reason_code} references: 0
    (F3 standardization on `code` preserved)
  - JSON example invalid-syntax fix from prior round preserved (F4)

Also abandoned the unpushed evidence-schema-extension follow-up
branch — it would have widened ErrorResponseMirror.error to include
the three v0.1.26 codes, but with v0.1.26 unimplemented, doing so
would add forward-compat schema slots that no real envelope
exercises. Same logic that drove the doc scope-out.

The prior round's other fixes are preserved unchanged:

  - F3 (`code` standardization across cycles.denial_detail field
    names): kept
  - F4 (JSON example uses `"ErrorCode"` instead of TS union
    syntax): kept
  - Previously self-audited fixes (hooks.ts line citations,
    DenialReason vs TierOneDenial type name): kept

Memory rule update: the rigor protocol's adversarial-pass step now
includes a deployment-reality check on top of the canonical-spec
check. Spec text can describe contracts that no deployment exercises
yet; documenting them in a v0.1 mapping doc creates phantom rows.
@amavashev
Copy link
Copy Markdown
Contributor Author

Course-correction on the reviewer round 1 response: I addressed the v0.1.26 finding incorrectly. Fixed in 2605a17.

What changed

The reviewer's finding #1 ("v0.1.26 codes appear in DecisionReasonCode table but not in ErrorCode table — spec-consistency bug") had two possible resolutions:

  1. Add v0.1.26 rows to ErrorCode table (what 89c1aeb did)
  2. Remove v0.1.26 rows from DecisionReasonCode table

I picked #1, treating the spec text as ground truth. But the user pointed out that cycles-protocol-extensions-v0.1.26 is published in the repo but not implemented in any Cycles deployment yet — so resolution #1 documents a contract that no envelope exercises and no fixture can verify against. The right resolution was #2.

What 2605a17 does

  • Mapping table 1: back to 15 ErrorCode rows (was 18 in 89c1aeb, originally 15 in 0b57d24)
  • Mapping table 2: back to 6 DecisionReasonCode rows (was 9, originally 9)
  • TS const tables: 15 + 6 entries (was 18 + 9)
  • Removed: previous "v0.1 evidence-schema dependency note" and "DenyDetail out of scope" subsections — consolidated into a single new section "v0.1.26 extension is out of scope for v0.1" with a 4-step v0.2 promotion criterion that triggers when a deployment ships v0.1.26
  • Abandoned: the unpushed follow-up branch that would have widened ErrorResponseMirror.error enum in the merged evidence schema. With v0.1.26 unimplemented, widening the schema would add forward-compat slots no real envelope exercises.

Preserved from the prior round

  • F3 (standardize on cycles.denial_detail.code field name): kept; 0 stray references
  • F4 (JSON example uses "ErrorCode" literal, not TS union syntax): kept; JSON example parses cleanly
  • Self-audit fixes (hooks.ts line citations at L87-L94 / L381 / L606 / L629, DenialReason type name): kept

Validation

  • 15 ErrorCode rows in mapping table 1
  • 6 DecisionReasonCode rows in mapping table 2
  • 15 entries in TS ERROR_CODE_TO_TIER1
  • 6 entries in TS DECISION_REASON_TO_TIER1
  • 0 v0.1.26 extension codes referenced anywhere in the doc except the explicit out-of-scope section
  • JSON example parses cleanly
  • 0 stray cycles.denial_detail.error / .reason_code references

Process note

Adding rule to my review-rigor memory: the adversarial-pass step now includes a deployment-reality check alongside the canonical-spec check. A published spec can describe contracts that no deployment exercises yet. Documenting them in a v0.1 mapping doc creates phantom rows — looks rigorous on paper, but the TS const becomes a fiction the adapter author can't actually exercise. Cross-check spec coverage against actual deployment status before treating canonical text as ground truth.

The reviewer was right to flag the inconsistency. The choice of resolution required user-level context that I didn't have until the intervention.

@amavashev
Copy link
Copy Markdown
Contributor Author

Quick confirmation — all three findings are already addressed on the current branch tip 2605a17. Looks like the review snapshot predates that commit (same pattern as #90 round 4, where a re-fetch resolved). Empirical state of HEAD:

F1 — v0.1.26 DecisionReasonCode rows in adapter contract: Removed in 2605a17. Mapping table 2 now has 6 rows (v0.1.25 base only); 0 v0.1.26 codes. The ### v0.1.26 extension is out of scope for v0.1 section explicitly lists the three extension codes (ACTION_QUOTA_EXCEEDED, ACTION_KIND_DENIED, ACTION_KIND_NOT_ALLOWED) as v0.2 promotion criteria, not adapter-contract requirements. The TS const DECISION_REASON_TO_TIER1 matches at 6 entries.

F2 — Tier-2 field naming inconsistency: Standardized on cycles.denial_detail.code in 89c1aeb. Grep result on current HEAD:

cycles.denial_detail.error  references: 0
cycles.denial_detail.reason_code references: 0
cycles.denial_detail.code   references: 4

F3 — JSON example using TS union syntax: Fixed in 89c1aeb. The example now uses "source": "ErrorCode" (concrete literal). json.loads() on the block parses cleanly. The two source values (ErrorCode vs DecisionReasonCode) are documented as prose immediately below the block rather than in the example itself.

Commit chain on the branch

2605a17 fix(drafts): scope to v0.1.25 only (revert v0.1.26 additions)
89c1aeb fix(drafts): address 4 review findings
78803dc fix(drafts): audit-driven corrections
0b57d24 feat(drafts): initial draft

If you can pull the branch and re-verify against 2605a17, the three findings should resolve. If anything still reads off after a refresh, happy to dig further — but the empirical state of the live branch right now is clean on all three.

…ound 4)

Two reviewer findings, both empirically verified against
cycles-protocol-v0.yaml ERROR SEMANTICS section before fixing.

1. HTTP status column in mapping table 1 had two spec-inaccuracies:

   - RESERVATION_EXPIRED listed as 409. Canonical L55 says
     "Expired reservations MUST return HTTP 410 with
     error=RESERVATION_EXPIRED" — corrected to 410.
   - UNIT_MISMATCH listed as 409. Canonical L59 says
     "Unit mismatch MUST return HTTP 400 with error=UNIT_MISMATCH"
     — corrected to 400.

   Per the rigor protocol, also did an adversarial sweep on the other
   HTTP status entries (which had been imprecise "4xx" / "5xx"
   categories) and tightened to specific canonical-cited or
   HTTP-standard values:

   - INVALID_REQUEST: 400 (canonical L614, L1782)
   - UNAUTHORIZED:    401 (HTTP standard; no explicit canonical)
   - FORBIDDEN:       403 (canonical L30, L1356)
   - NOT_FOUND:       404 (canonical L57)
   - BUDGET_FROZEN:   409 (inferred — same pattern as BUDGET_EXCEEDED L48)
   - BUDGET_CLOSED:   409 (inferred)
   - RESERVATION_FINALIZED: 409 (canonical L54)
   - IDEMPOTENCY_MISMATCH:  409 (canonical L99)
   - OVERDRAFT_LIMIT_EXCEEDED: 409 (canonical L49-L51, L80)
   - DEBT_OUTSTANDING: 409 (canonical L52)
   - MAX_EXTENSIONS_EXCEEDED: 409 (inferred)
   - INTERNAL_ERROR:  500 (HTTP standard)

   Added an HTTP-status column key noting that codes without explicit
   canonical citation are flagged "(inferred)" — the doc is honest
   about which statuses are spec-pinned vs. pattern-derived.

2. "Unknown DecisionReasonCode values" section still said "outside the
   9 known values" — leftover from the pre-v0.1.26-scope-out wording.
   Corrected to "outside the 6 known v0.1.25 base values" matching
   the rest of the doc.

Validation (all empirical):

  - 15 ErrorCode rows in mapping table 1, all with specific HTTP
    status codes
  - RESERVATION_EXPIRED: now 410 (canonical L55)
  - UNIT_MISMATCH: now 400 (canonical L59)
  - 0 "9 known values" references; 3 "6 known values" references
  - 0 stray cycles.denial_detail.error / .reason_code references
    (F3 standardization from prior round preserved)
  - JSON example still parses cleanly (F4 fix preserved)

Process note: missing the HTTP status codes was a coverage-gap class
of bug I should have caught in my own audit. The rigor rule (cross-
check every literal value against canonical) applies to status-code
columns the same way it applies to line citations and enum values.
Adding to memory.
@amavashev
Copy link
Copy Markdown
Contributor Author

Both findings confirmed and fixed in 1e3dae9. Empirically verified against cycles-protocol-v0.yaml ERROR SEMANTICS section before fixing.

Finding 1 — HTTP status codes

Confirmed:

  • RESERVATION_EXPIRED: canonical L55 says MUST return HTTP 410 with error=RESERVATION_EXPIRED (not 409). Fixed.
  • UNIT_MISMATCH: canonical L59 says MUST return HTTP 400 with error=UNIT_MISMATCH (not 409). Fixed.

While in the column, also did an adversarial sweep on the other HTTP statuses (which had been imprecise 4xx / 5xx categories) and tightened to specific values with explicit citation where the canonical pins them:

ErrorCode HTTP status Source
INVALID_REQUEST 400 canonical L614, L1782
UNAUTHORIZED 401 (inferred — HTTP standard)
FORBIDDEN 403 canonical L30, L1356
NOT_FOUND 404 canonical L57
BUDGET_EXCEEDED 409 canonical L48
BUDGET_FROZEN 409 (inferred — BUDGET_EXCEEDED pattern)
BUDGET_CLOSED 409 (inferred — BUDGET_EXCEEDED pattern)
RESERVATION_EXPIRED 410 canonical L55 ← fixed
RESERVATION_FINALIZED 409 canonical L54
IDEMPOTENCY_MISMATCH 409 canonical L99
UNIT_MISMATCH 400 canonical L59 ← fixed
OVERDRAFT_LIMIT_EXCEEDED 409 canonical L49-L51, L80
DEBT_OUTSTANDING 409 canonical L52
MAX_EXTENSIONS_EXCEEDED 409 (inferred — reservation-state pattern)
INTERNAL_ERROR 500 (inferred — HTTP standard)

Added a column-key note above the table making the canonical-explicit vs. inferred distinction clear, so downstream readers know which statuses are spec-pinned and which are pattern-derived.

Finding 2 — "9 known values" stale wording

Confirmed: the "Unknown DecisionReasonCode values" section still said "outside the 9 known values" — leftover from the pre-2605a17 (pre-v0.1.26-scope-out) wording. Fixed to "outside the 6 known v0.1.25 base values" matching the rest of the doc.

Validation

  • 15 ErrorCode rows in mapping table 1, all with specific HTTP status codes
  • 0 "9 known values" references (was 1); 3 "6 known values" references
  • 0 stray cycles.denial_detail.error / .reason_code references (prior-round F3 fix preserved)
  • JSON example still parses cleanly (prior-round F4 fix preserved)

Process note

HTTP status codes are a literal-value column I should have cross-checked against the canonical pre-commit — same class of bug as the /v1/decisions typo from PR #90 round 5. Updating my review-rigor memory with this: every column in every mapping table is a stack of literal claims, all of which need canonical verification, not just enum names and line citations.

…e confusion

Reviewer flagged that the doc said "APS PaymentReceipt denial_reason
values" and "APS PaymentReceipt with the mapped Tier-1 reason" —
implying the success-path receipt type carries denial fields. It
doesn't.

Verified against `aeoess/agent-passport-system/src/v2/payment-rails/types.ts`:

  - PaymentReceipt (L92): proof of rail event occurrence. Success
    path only. No denial_reason field.
  - PaymentDenial (L137): closed-taxonomy denial with denial_reason.
  - GovernanceHooks splits the two emit paths (L340 emitReceipt,
    L344 emitDenial), both returning their respective top-level type.

The header comment in types.ts L13-L18 makes the distinction
explicit: success → "fully-signed PaymentReceipt that binds (rail_name,
action_ref, delegation_ref, amount, currency, tx_proof)"; failure →
"fully-signed PaymentDenial citing one of the closed-taxonomy
denial_reason values."

Three occurrences fixed (reviewer flagged two; sweep found a third):

  1. Purpose paragraph: "APS PaymentReceipt Tier-1 denial_reason
     values" → "APS PaymentDenial Tier-1 denial_reason values".
     Plus added an explanatory note clarifying the two types are
     distinct and citing the four canonical line numbers.

  2. TS comment in the reference impl (was undetected by reviewer —
     said `PaymentDenialReceipt envelope`, which doesn't exist as a
     type name at all). Fixed to `PaymentDenial envelope`.

  3. Round-trip verification section: "Cycles denial → CyclesEvidence
     envelope → APS PaymentReceipt with the mapped Tier-1 reason"
     → "APS PaymentDenial with the mapped Tier-1 reason".

Verified post-fix:

  - 3 PaymentReceipt references remaining (all in the new explanatory
    note that *explicitly* distinguishes the two types — correct usage,
    not the conflation pattern)
  - 3 PaymentDenial references at the operative locations (purpose,
    TS comment, round-trip section)
  - PaymentDenialReceipt: 0 references (was 1, fixed)
  - Line citations (L92, L137, L340, L344) all verified via `gh api`
    against current types.ts on aeoess/agent-passport-system main

Process note: this is the third round-trip where the reviewer caught
a cross-repo terminology slip I should have caught via rule 7 (fetch
+ grep cross-repo type names before commit). The pattern is consistent:
I keep using approximate-sounding names from memory of earlier
conversations (`PaymentDenialReceipt`, `TierOneDenial`, `hooks.ts:115-123`)
without re-verifying. Updating memory rule 7 to make the verification
explicit — not just for ImportedType names but also for prose
references to top-level type names.
@amavashev
Copy link
Copy Markdown
Contributor Author

Finding confirmed and fixed in ab403ca. Verified against aeoess/agent-passport-system/src/v2/payment-rails/types.ts via gh api:

  • PaymentReceipt (L92): success-path proof of rail event occurrence. No denial_reason field.
  • PaymentDenial (L137): denial with closed-taxonomy denial_reason.
  • GovernanceHooks splits the two emit paths: emitReceipt (L340) returns PaymentReceipt; emitDenial (L344) returns PaymentDenial.

The types.ts header comment at L13-L18 makes the split explicit. My doc was conflating them.

Fixed (3 occurrences — reviewer flagged 2, sweep caught 1 more)

  1. Purpose paragraph (line 8): "APS PaymentReceipt Tier-1 denial_reason""APS PaymentDenial Tier-1 denial_reason". Added an explanatory note citing the four canonical line numbers so future readers don't replicate the conflation.

  2. TS reference impl comment (line 230): had "PaymentDenialReceipt envelope" — that string isn't a real APS type at all (it's a memory blend of PaymentReceipt + PaymentDenial). Fixed to "PaymentDenial envelope". This was the same conflation class as your finding; sweep caught it.

  3. Round-trip verification section (line 372): "APS PaymentReceipt with the mapped Tier-1 reason""APS PaymentDenial with the mapped Tier-1 reason".

Post-fix state

  • 3 PaymentReceipt references remain, all inside the new explanatory note that explicitly distinguishes the two types (correct usage, not conflation)
  • 3 PaymentDenial references at the operative locations
  • 0 PaymentDenialReceipt references (was 1)
  • All four cited line numbers (types.ts L92, L137, L340, L344) verified against the current file via gh api

Process note

This is the third round where the reviewer caught a cross-repo terminology slip I should have caught with my own rule 7 (gh api fetch + grep before commit on every cross-repo name). I keep trusting memory of approximate names — PaymentDenialReceipt, TierOneDenial (from round-1 self-audit), hooks.ts:115-123 (also round-1 self-audit). Tightening the rule: every top-level type name from another repo, including in prose, gets re-fetched and grepped before commit.

@amavashev amavashev merged commit 9b0fb5e into main May 13, 2026
5 checks passed
@amavashev amavashev deleted the feat/cycles-aps-denial-mapping-v0.1-draft branch May 13, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant