Skip to content

docs: add specification for the error catalogue in the SDK - #1266

Draft
ogenstad wants to merge 5 commits into
infrahub-developfrom
pog-error-catalogue-IFC-3034
Draft

docs: add specification for the error catalogue in the SDK#1266
ogenstad wants to merge 5 commits into
infrahub-developfrom
pog-error-catalogue-IFC-3034

Conversation

@ogenstad

@ogenstad ogenstad commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Specification only — no behaviour change. Adds dev/specs/ifc-3034-error-catalogue/ covering how the SDK consumes Infrahub's GraphQL error catalogue, so that ordinary operations raise the specific error for the failure. GraphQLError remains the fallback for failures the catalogue does not cover and stays the common base class, so existing except GraphQLError code keeps working.

Ref: IFC-3034. Related: IFC-2279 (spike), INFP-468 (backend catalogue), GitHub #7498 (out of scope).

Decisions settled while drafting

  • A new ApiError base above both AuthenticationError and GraphQLError. Authentication failures reach consumers from the REST path as well as GraphQL, so they cannot simply be re-rooted under GraphQLError. Verified that a 401/403 on a GraphQL call is already handled as an httpx.HTTPStatusError and raises AuthenticationError before the body is parsed for GraphQL errors — so except GraphQLError never caught auth failures, and no dual inheritance is needed to preserve compatibility.
  • .code is a catalogue string or None. The /api/... envelope's extensions.code is an integer mirroring the HTTP status, a different thing with a different type; it is not surfaced through .code. The catalogue is GraphQL-only today.
  • Generated classes derive their parent from the declared HTTP status — 401/403 under the authentication branch, everything else under GraphQLError — rather than a hand-maintained per-code mapping.
  • Infrahub generates the bindings into this repo as its python_sdk submodule, matching how protocols.py and the generated schema models already arrive. No copy of the catalogue schema is vendored here, so there is one freshness invariant instead of two, policed by extending Infrahub's existing validate-generated check. No release-time gate is added on either side.
  • Query text is dropped from the message for catalogued errors only; uncatalogued errors keep today's message verbatim.
  • NodeNotFoundError, BranchNotFoundError and SchemaNotFoundError are unified with their catalogue counterparts and re-rooted under GraphQLError, accepting that except GraphQLError now also catches client-side lookup misses.

Findings from the code survey worth a reviewer's eye

These are in the spec's Edge Cases section as specific hazards, not hypotheticals:

  • An ordered isinstance ladder gets shadowed. infrahub_sdk/ctl/utils.py:58-72 tests GraphQLError at line 67 before (SchemaNotFoundError, NodeNotFoundError, ...) at line 70. Re-rooting those classes makes the later branch unreachable, silently changing CLI output for exactly the errors this feature makes specific. FR-018 requires the correction.
  • A renderer with no server errors to render. That same GraphQLError branch renders exc.errors, a list of server error dicts. A unified NodeNotFoundError raised purely client-side has no server response behind it, so the list is empty.
  • identifier carries two types. The existing client-side NodeNotFoundError has identifier as a mapping of filters; the catalogue payload has it as a single string. FR-016 mandates the unification; the reconciliation mechanism is left to the plan.
  • Eight more, including NodeInvalidError silently inheriting the re-rooting, a pre-existing call site passing a string where GraphQLError expects a list of error dicts, UNDEFINED_ERROR being a real code rather than the absence of one, and GraphQL data errors arriving as HTTP 200 while auth failures arrive as real 401/403 on a separate code path.

Scope

Six prioritised user stories, 28 functional requirements. FR-025 to FR-027 land in the Infrahub repository (generation plus the extended drift check) and are tagged as such; everything else lands here.

Checks

rumdl clean across 131 files; Vale flags nothing in the new files. Requirements checklist at dev/specs/ifc-3034-error-catalogue/checklists/requirements.md passes 16/16 with no [NEEDS CLARIFICATION] markers remaining.


Summary by cubic

Documents the IFC-3034 design for consuming Infrahub's GraphQL error catalogue in the Python SDK. This PR adds specifications and implementation guidance only; it does not change SDK behavior.

Proposed design

  • Adds ApiError above GraphQLError and AuthenticationError, with catalogue errors exposing typed payload fields directly.
  • Uses a shared factory for sync and async clients, preserving full error lists and falling back safely for unknown codes, invalid payloads, and older servers.
  • Keeps catalogue handling GraphQL-only, leaves REST's legacy envelope unchanged, and uses a string .code or None.
  • Defines first-error precedence, not-found exception unification, query-message changes for catalogued errors, and import and isinstance compatibility requirements.
  • Specifies generated bindings in the python_sdk submodule and assigns generation and drift validation to Infrahub under FR-025–FR-027.
  • Includes the IFC-3034 specification, data model, contracts, research, plan, critique, checklist, and validation quickstart.

Review fixes

  • Corrected plan constraints contradicting FR-004 and FR-012, and documented code on the adopted classes as always present or None.
  • Qualified the Messages guarantee around catalogue codes rather than server responses.
  • Corrected the critique's trend paragraph to match its section's "most" rather than "all".
  • Removed an Any contradiction in data-model.md and a local developer path; pinned the catalogue survey to opsmill/infrahub@develop since counts and the UNIQUENESS_VIOLATION example differ on the stable line.
  • Dropped the three 401/403 exception classes that inherited from both branches; the hierarchy is now single-inheritance, and FR-005, FR-008, SC-001, and US3 were amended to match.
  • Made promoted attributes on NodeNotFoundError, BranchNotFoundError, and SchemaNotFoundError optional, since neither provenance populates the full set; a split would be more expensive, so separate SDK-own classes are deferred to a later change.

Written for commit 04b273f. Summary will update on new commits.

Review in cubic

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 04b273f
Status: ✅  Deploy successful!
Preview URL: https://5685ba83.infrahub-sdk-python.pages.dev
Branch Preview URL: https://pog-error-catalogue-ifc-3034.infrahub-sdk-python.pages.dev

View logs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/spec.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/checklists/requirements.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="dev/specs/ifc-3034-error-catalogue/quickstart.md">

<violation number="1" location="dev/specs/ifc-3034-error-catalogue/quickstart.md:1">
P3: This is a pure documentation/specification change (dev/specs/...), so it can't affect a running product and should ship on the stable release vehicle rather than the develop train. Per the release-vehicle guideline, pure docs changes belong on stable; target develop instead.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/research.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/spec.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md Outdated
@@ -0,0 +1,196 @@
# Quickstart: validating the error catalogue in the SDK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This is a pure documentation/specification change (dev/specs/...), so it can't affect a running product and should ship on the stable release vehicle rather than the develop train. Per the release-vehicle guideline, pure docs changes belong on stable; target develop instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ifc-3034-error-catalogue/quickstart.md, line 1:

<comment>This is a pure documentation/specification change (dev/specs/...), so it can't affect a running product and should ship on the stable release vehicle rather than the develop train. Per the release-vehicle guideline, pure docs changes belong on stable; target develop instead.</comment>

<file context>
@@ -0,0 +1,196 @@
+# Quickstart: validating the error catalogue in the SDK
+
+Runnable checks that prove the feature works end to end. Each scenario names what it proves and the
</file context>

Comment thread dev/specs/ifc-3034-error-catalogue/plan.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/data-model.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/data-model.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/spec.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and no new issues found across 5 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/spec.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/contracts/generator-contract.md Outdated
Specification, implementation plan, research decisions, data model, interface
contracts, and a validation quickstart for making ordinary SDK operations raise
the specific exception for the failure the server reported, on both the async and
sync clients, without ever raising on a payload the SDK does not recognise.

The design in brief:

- A new `ApiError` base carries the parsed envelope — catalogue code, declared
  HTTP status, raw extensions, and the server's error list — with `GraphQLError`
  and `AuthenticationError` descending from it. One raise-time factory serves
  every existing raise site, so the code is readable against any server version
  even with no generated bindings present.
- A catalogued error's payload is read as directly typed attributes on the
  exception (`exc.node_kind`, `exc.fields`), typed exactly as the catalogue
  declares them. The pydantic payload model validates the envelope and populates
  them; it is not the access path. Nothing is typed `Any` beyond raw decoded
  JSON, and no type-check suppression is anticipated.
- `infrahub_sdk/exceptions.py` becomes a strictly layered package — hand-written
  base, generated catalogue, factory, façade — with imports pointing only
  downward and a test enforcing it. `infrahub_sdk.exceptions` remains the one
  supported import path, and a snapshot test pins that no name importable from it
  disappears.
- Codes declaring 401 or 403 descend from both branches. Infrahub returns HTTP
  200 for resolver-raised errors, so a permission failure arrives on the data
  path inside a response `except GraphQLError` catches today.
- The raised class is a function of the response's first error code and the
  transport observed — never of payload validity, binding freshness, or a code's
  declared status.
- Infrahub generates the bindings into the submodule from `backend.generate`,
  alongside the schema models and protocols it already generates there, and
  `backend.validate-generated` fails when they are stale.

Three broadenings are accepted deliberately and recorded in the spec, together
with the `NodeNotFoundError.identifier` widening that unification forces.

Requirements FR-025 to FR-027 land in the Infrahub repository; everything else
lands here.
@ogenstad
ogenstad force-pushed the pog-error-catalogue-IFC-3034 branch from a1e35dd to a35e9c1 Compare September 2, 2026 16:04

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/research.md
Comment thread dev/specs/ifc-3034-error-catalogue/research.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/plan.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/data-model.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/quickstart.md
Comment thread dev/specs/ifc-3034-error-catalogue/plan.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/spec.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/spec.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/data-model.md Outdated
Seven valid findings, three rejected. Most had been present since the original
plan commit and survived six incremental reviews, which only ever read the delta.

Valid:

- The plan's Constraints section still said the raised class never depends on
  payload validity or on the transport observed, contradicting FR-004 and
  FR-012. Stale since the E3 reversal, and it would have led an implementer to
  route a payload-invalid 401/403 code to the authentication branch and lose the
  `except GraphQLError` coverage FR-018 requires.
- Nothing documented set `code` on the three adopted classes, so the
  `exc.code is not None` test for "came from the server" could not work. The
  factory sets it per-instance there; a class attribute cannot, since the same
  class must report None on a client-side raise.
- FR-002's "the base GraphQL error itself" was ambiguous once FR-001 introduced a
  shared base above both branches. It now names that base and excludes the
  per-code classes.
- FR-003 and FR-012 described `code` as "absent" while two acceptance scenarios
  promise `None` — different observable contracts. Standardised on
  always-exists-and-may-be-None.
- The Messages guarantee claimed every catalogued failure names the server's
  message, but three catalogued classes can be raised client-side with no server
  response. Qualified in FR-022 and the contract.
- data-model.md said "nothing here is typed Any" two paragraphs above an
  `extensions: dict[str, Any]` row.
- A local absolute developer path in plan.md.

Rejected, but the gap behind two of them fixed: the catalogue counts and the
`UNIQUENESS_VIOLATION` example are correct for `opsmill/infrahub@develop`, which
this feature pairs with, and wrong only against the stable line the reviewer
read. Nothing in the artefacts said which ref the numbers came from, so the
survey now pins `develop` and records that US1 scenario 1 needs a catalogue
containing that code. The third rejection — that the "Expired Signature" grep
should expect two sites — misses that R10 puts both the code check and the legacy
fallback in one shared helper; R10 and the quickstart now say so explicitly.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread dev/specs/ifc-3034-error-catalogue/critiques/critique-20260824-161725.md Outdated
Comment thread dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md Outdated
The Messages qualifier said the three unified classes are raised "with no server
response behind them", but the file handler raises NodeNotFoundError from a REST
404 that does carry a response and a message — a case the broadenings list two
paragraphs above. Restated around the operative property instead: no catalogue
code behind them, which covers both the client-side miss and the REST 404, whose
response carries the legacy envelope. FR-022 corrected the same way.

The critique's trend paragraph also claimed all seven ninth-pass defects had
survived since the original plan commit, while the section it summarises says
"most" — and its own example only became a contradiction at the E3 reversal.
The three 401/403 codes no longer get their own exception class. They are the
only codes that reach the SDK on two different transports, and each transport
already has a class an existing `except` clause depends on: a real 401/403 raises
AuthenticationError, and a resolver-raised failure inside an HTTP 200 raises
GraphQLError. The transport rule already in FR-012 picks the right one, and
`exc.code` carries the identity.

One class per code could satisfy both only by inheriting from both branches. That
diamond had already produced three defects, all found in review rather than by
design: the positional-argument corruption, since method resolution handed those
classes GraphQLError.__init__ whose first parameter is `errors`; the cooperative
super().__init__ reaching AuthenticationError.__init__ on the GraphQL path, where
its default message escaped substitution only by accident; and two generated
class shapes instead of one. It also misread as GraphQLError descending from
AuthenticationError on first contact. All to distinguish three codes whose
payloads are empty or entirely nullable and usually unset.

The hierarchy is now a tree with no class having more than one parent. FR-008
changes from "which parents" to "which codes get classes" — one rule, and the
factory needs no special case, since the lookup simply misses for those codes.

Amended: US3's acceptance scenarios, which specified distinct types before the
HTTP 200 behaviour was verified, plus FR-005, FR-008, SC-001, and the Key
Entities description. The third broadening added in the previous round
disappears, because a real 401/403 once again produces AuthenticationError and
nothing else.

Accepted cost: those three codes have no typed payload attributes, reachable only
through `exc.extensions["data"]` if the catalogue later gives them substantive
fields.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md">

<violation number="1" location="dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md:23">
P2: The plain-tree change contradicts the promise in the catching table. The intent row "Any authentication or permission failure, either transport | except AuthenticationError" (and the code example that only handles AuthenticationError) no longer holds: per the new arrival table, a resolver-raised 401/403 that arrives inside an HTTP 200 `errors` array is raised as `GraphQLError`, which `except AuthenticationError` does not catch. Before this change those classes satisfied both bases, so `except AuthenticationError` caught both transports; that coverage is now gone while the table and the FR-018 claim ("every clause that worked before the change still catches what it caught before") still assert it. Consumers handling permission failures with `except AuthenticationError` will silently miss the resolver-raised case. Either update the intent row and the FR-018 claim to say `except GraphQLError` (or both clauses) is required for GraphQL-transport auth failures, or note the loss as a deliberate narrowing.</violation>
</file>

<file name="dev/specs/ifc-3034-error-catalogue/research.md">

<violation number="1" location="dev/specs/ifc-3034-error-catalogue/research.md:269">
P3: R5's decision text says 'twelve generated classes under GraphQLError', but only nine are generated. Per R4, the three adopted classes (NodeNotFoundError, BranchNotFoundError, SchemaNotFoundError) are hand-written and the generator emits an import, not a class definition, for them. The diagram two lines below correctly shows 'three adopted + nine more generated'. Align the wording so the implementer counts generated classes correctly.</violation>
</file>

<file name="dev/specs/ifc-3034-error-catalogue/data-model.md">

<violation number="1" location="dev/specs/ifc-3034-error-catalogue/data-model.md:76">
P2: The new AuthenticationError text says "It gains no subclasses", but spec.md FR-015 still promises the opposite: "MUST ... remain the class raised for REST authentication failures, while gaining the three catalogue subclasses beneath it" (dev/specs/ifc-3034-error-catalogue/spec.md:352). At the current PR head the two spec documents contradict each other on whether AuthenticationError has subclasses. Update FR-015 to drop "while gaining the three catalogue subclasses beneath it", and audit FR-012's rationale, which still refers to "the dual inheritance in FR-008" after FR-008's dual-inheritance wording was removed.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

| One of the three authentication codes | `except AuthenticationError` then test `exc.code` |
| Anything the SDK raises | `except Error` |

The hierarchy is a plain tree: `GraphQLError` and `AuthenticationError` are siblings under `ApiError`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The plain-tree change contradicts the promise in the catching table. The intent row "Any authentication or permission failure, either transport | except AuthenticationError" (and the code example that only handles AuthenticationError) no longer holds: per the new arrival table, a resolver-raised 401/403 that arrives inside an HTTP 200 errors array is raised as GraphQLError, which except AuthenticationError does not catch. Before this change those classes satisfied both bases, so except AuthenticationError caught both transports; that coverage is now gone while the table and the FR-018 claim ("every clause that worked before the change still catches what it caught before") still assert it. Consumers handling permission failures with except AuthenticationError will silently miss the resolver-raised case. Either update the intent row and the FR-018 claim to say except GraphQLError (or both clauses) is required for GraphQL-transport auth failures, or note the loss as a deliberate narrowing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md, line 23:

<comment>The plain-tree change contradicts the promise in the catching table. The intent row "Any authentication or permission failure, either transport | except AuthenticationError" (and the code example that only handles AuthenticationError) no longer holds: per the new arrival table, a resolver-raised 401/403 that arrives inside an HTTP 200 `errors` array is raised as `GraphQLError`, which `except AuthenticationError` does not catch. Before this change those classes satisfied both bases, so `except AuthenticationError` caught both transports; that coverage is now gone while the table and the FR-018 claim ("every clause that worked before the change still catches what it caught before") still assert it. Consumers handling permission failures with `except AuthenticationError` will silently miss the resolver-raised case. Either update the intent row and the FR-018 claim to say `except GraphQLError` (or both clauses) is required for GraphQL-transport auth failures, or note the loss as a deliberate narrowing.</comment>

<file context>
@@ -14,36 +14,40 @@ than asserted.
 
-The catalogued 401/403 classes deliberately satisfy both `except GraphQLError` and
-`except AuthenticationError`, because they reach the SDK on the GraphQL transport two different ways:
+The hierarchy is a plain tree: `GraphQLError` and `AuthenticationError` are siblings under `ApiError`,
+and no class has more than one parent.
 
</file context>


## AuthenticationError

Name, constructor, and default message unchanged (FR-015). It gains no subclasses and inherits the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new AuthenticationError text says "It gains no subclasses", but spec.md FR-015 still promises the opposite: "MUST ... remain the class raised for REST authentication failures, while gaining the three catalogue subclasses beneath it" (dev/specs/ifc-3034-error-catalogue/spec.md:352). At the current PR head the two spec documents contradict each other on whether AuthenticationError has subclasses. Update FR-015 to drop "while gaining the three catalogue subclasses beneath it", and audit FR-012's rationale, which still refers to "the dual inheritance in FR-008" after FR-008's dual-inheritance wording was removed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ifc-3034-error-catalogue/data-model.md, line 76:

<comment>The new AuthenticationError text says "It gains no subclasses", but spec.md FR-015 still promises the opposite: "MUST ... remain the class raised for REST authentication failures, while gaining the three catalogue subclasses beneath it" (dev/specs/ifc-3034-error-catalogue/spec.md:352). At the current PR head the two spec documents contradict each other on whether AuthenticationError has subclasses. Update FR-015 to drop "while gaining the three catalogue subclasses beneath it", and audit FR-012's rationale, which still refers to "the dual inheritance in FR-008" after FR-008's dual-inheritance wording was removed.</comment>

<file context>
@@ -74,9 +73,15 @@ default.
-Name, constructor, and default message unchanged (FR-015). It remains the class raised for REST
-authentication failures, where `code` is `None`. It gains the three generated subclasses and the
-inherited `ApiError` attributes.
+Name, constructor, and default message unchanged (FR-015). It gains no subclasses and inherits the
+`ApiError` attributes. It remains the class raised for every failure the SDK observed as HTTP 401 or
+403, with `code` set per-instance by the factory: `None` on the REST path, which carries no catalogue
</file context>

parent it takes. A code declaring 401 or 403 gets **no class**; every other code gets one, descending
from `GraphQLError` alone. No class in the design has more than one parent.

Today that yields twelve generated classes under `GraphQLError`, and `AUTHENTICATION_REQUIRED`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: R5's decision text says 'twelve generated classes under GraphQLError', but only nine are generated. Per R4, the three adopted classes (NodeNotFoundError, BranchNotFoundError, SchemaNotFoundError) are hand-written and the generator emits an import, not a class definition, for them. The diagram two lines below correctly shows 'three adopted + nine more generated'. Align the wording so the implementer counts generated classes correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ifc-3034-error-catalogue/research.md, line 269:

<comment>R5's decision text says 'twelve generated classes under GraphQLError', but only nine are generated. Per R4, the three adopted classes (NodeNotFoundError, BranchNotFoundError, SchemaNotFoundError) are hand-written and the generator emits an import, not a class definition, for them. The diagram two lines below correctly shows 'three adopted + nine more generated'. Align the wording so the implementer counts generated classes correctly.</comment>

<file context>
@@ -260,40 +260,84 @@ hand-written unified classes subclass them. Rejected — the resolution map woul
+parent it takes. A code declaring 401 or 403 gets **no class**; every other code gets one, descending
+from `GraphQLError` alone. No class in the design has more than one parent.
+
+Today that yields twelve generated classes under `GraphQLError`, and `AUTHENTICATION_REQUIRED`,
+`TOKEN_EXPIRED`, and `PERMISSION_DENIED` carrying their identity in `exc.code` on whichever generic
+class their transport already produces.
</file context>
Suggested change
Today that yields twelve generated classes under `GraphQLError`, and `AUTHENTICATION_REQUIRED`,
Today that yields nine generated classes plus the three adopted under `GraphQLError`, and `AUTHENTICATION_REQUIRED`,

Neither provenance can populate a unified class's full attribute set, and they
fail to in both directions: the catalogue supplies no `branch_name` at all, while
four of the seven client-side raise sites supply no node kind and fall back to
"unknown". So a promoted attribute on `NodeNotFoundError`,
`BranchNotFoundError`, or `SchemaNotFoundError` must be optional even where the
catalogue declares the underlying field required — a required attribute is a
promise a class raised from two provenances cannot keep.

Whether the SDK could fill a future catalogue field client-side depends entirely
on the field, which is why the policy has to be optional-by-default rather than
decided per field. Recorded with a tripwire for revisiting: an adopted code
gaining a field that is both required and semantically server-only.

Also corrects this plan's own framing of R9. Earlier rounds treated the
unification as the design's weak point. A split is in fact more expensive than
the optional attribute, because the derived name for NODE_NOT_FOUND is the
existing class name — so splitting means breaking FR-005, FR-006, or every
`except NodeNotFoundError` around a store lookup.

Records the direction of travel the maintainer set: separate classes for the
SDK's own failures, which would retire `identifier`'s dual meaning and make
`branch_name` coherent, reached through the constitution's deprecation path as a
later change rather than inside this one.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md">

<violation number="1" location="dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md:60">
P3: The new row justifies the optional attributes as "because those classes are also raised without a server response", but the REST 404 path does carry a server response — it only lacks a catalogue code. The operative property, as the Messages section here (and critique-20260824-161725.md:411) already establishes, is "no catalogue code", not "no server response". Rephrase so the justification covers both the client-side miss and the REST 404: "whose attributes are optional because those classes are also raised with no catalogue code behind them".</violation>
</file>

<file name="dev/specs/ifc-3034-error-catalogue/research.md">

<violation number="1" location="dev/specs/ifc-3034-error-catalogue/research.md:551">
P3: The new table's counts of NodeNotFoundError client-side raise sites are inaccurate, and the optional-by-policy decision is justified on them. The repo actually has nine `raise NodeNotFoundError` sites (store.py:114/123/142/160/184, client.py:676/3273, file_handler.py:168, ctl/object/utils.py:79), not seven — store.py:184 (`_get_by_hfid`, passes `node_type`) and ctl/object/utils.py:79 (passes `node_type` and `branch_name`) are omitted. `node_type` is therefore supplied by five sites, not three; `identifier` is a filter mapping at five store sites, not four. Re-count against the current sources so the justification for making the promoted attributes optional rests on accurate figures.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

|-----------|----------|
| `code` | The catalogue code string, or `None`. Never an integer. `None` means the SDK resolved no catalogue code — a pre-catalogue server, a REST failure, an error with no `extensions`, or an integer `code` on the wire. An unrecognised string code from a newer server is still readable here. |
| `http_status` | The code's catalogue-declared status, or `None`. This is metadata about the failure, not the HTTP status of the response — a catalogued data error arrives as HTTP 200. Where the error carried an `extensions` mapping, the status the server actually returned is available as `exc.extensions["http_status"]` — guard on `exc.extensions` first, since it is `None` when the error carried none. The two can legitimately differ: the server replaces a declared 500 with the real HTTP status when it has a more accurate one. |
| the payload's fields | Not on the base. Each catalogued class carries its payload's fields as directly typed attributes — `UniquenessViolationError.node_kind` is a `str`, `.fields` a `list[str]` — typed exactly as the catalogue declares them, so a required field is never optional and needs no guard. The three exceptions are `NodeNotFoundError`, `BranchNotFoundError`, and `SchemaNotFoundError`, whose attributes are optional because those classes are also raised without a server response; guard on `exc.code is not None` there. The raw payload dict remains in `extensions["data"]` for anything forwarding it verbatim. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new row justifies the optional attributes as "because those classes are also raised without a server response", but the REST 404 path does carry a server response — it only lacks a catalogue code. The operative property, as the Messages section here (and critique-20260824-161725.md:411) already establishes, is "no catalogue code", not "no server response". Rephrase so the justification covers both the client-side miss and the REST 404: "whose attributes are optional because those classes are also raised with no catalogue code behind them".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ifc-3034-error-catalogue/contracts/exception-hierarchy.md, line 60:

<comment>The new row justifies the optional attributes as "because those classes are also raised without a server response", but the REST 404 path does carry a server response — it only lacks a catalogue code. The operative property, as the Messages section here (and critique-20260824-161725.md:411) already establishes, is "no catalogue code", not "no server response". Rephrase so the justification covers both the client-side miss and the REST 404: "whose attributes are optional because those classes are also raised with no catalogue code behind them".</comment>

<file context>
@@ -57,7 +57,7 @@ Available on every `ApiError`:
 | `code` | The catalogue code string, or `None`. Never an integer. `None` means the SDK resolved no catalogue code — a pre-catalogue server, a REST failure, an error with no `extensions`, or an integer `code` on the wire. An unrecognised string code from a newer server is still readable here. |
 | `http_status` | The code's catalogue-declared status, or `None`. This is metadata about the failure, not the HTTP status of the response — a catalogued data error arrives as HTTP 200. Where the error carried an `extensions` mapping, the status the server actually returned is available as `exc.extensions["http_status"]` — guard on `exc.extensions` first, since it is `None` when the error carried none. The two can legitimately differ: the server replaces a declared 500 with the real HTTP status when it has a more accurate one. |
-| the payload's fields | Not on the base. Each catalogued class carries its payload's fields as directly typed attributes — `UniquenessViolationError.node_kind` is a `str`, `.fields` a `list[str]` — typed exactly as the catalogue declares them, so a required field is never optional and needs no guard. The raw payload dict remains in `extensions["data"]` for anything forwarding it verbatim. |
+| the payload's fields | Not on the base. Each catalogued class carries its payload's fields as directly typed attributes — `UniquenessViolationError.node_kind` is a `str`, `.fields` a `list[str]` — typed exactly as the catalogue declares them, so a required field is never optional and needs no guard. The three exceptions are `NodeNotFoundError`, `BranchNotFoundError`, and `SchemaNotFoundError`, whose attributes are optional because those classes are also raised without a server response; guard on `exc.code is not None` there. The raw payload dict remains in `extensions["data"]` for anything forwarding it verbatim. |
 | `extensions` | The raw `extensions` mapping of the governing error, or `None`. |
 | `errors` | The complete server error list, unreordered — empty for a client-side raise. |
</file context>
Suggested change
| the payload's fields | Not on the base. Each catalogued class carries its payload's fields as directly typed attributes — `UniquenessViolationError.node_kind` is a `str`, `.fields` a `list[str]` — typed exactly as the catalogue declares them, so a required field is never optional and needs no guard. The three exceptions are `NodeNotFoundError`, `BranchNotFoundError`, and `SchemaNotFoundError`, whose attributes are optional because those classes are also raised without a server response; guard on `exc.code is not None` there. The raw payload dict remains in `extensions["data"]` for anything forwarding it verbatim. |
The three exceptions are `NodeNotFoundError`, `BranchNotFoundError`, and `SchemaNotFoundError`, whose attributes are optional because those classes are also raised with no catalogue code behind them; guard on `exc.code is not None` there.

| Attribute | Client-side raises | Catalogue payload |
|-----------|--------------------|-------------------|
| `identifier` | a filter mapping from four `store.py` sites and two `client.py` sites; a plain string from `file_handler.py:168` | a single string, declared required |
| `node_type` | supplied by three of seven raise sites; the four `store.py` sites omit it and fall back to `"unknown"` | `node_kind`, declared required |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new table's counts of NodeNotFoundError client-side raise sites are inaccurate, and the optional-by-policy decision is justified on them. The repo actually has nine raise NodeNotFoundError sites (store.py:114/123/142/160/184, client.py:676/3273, file_handler.py:168, ctl/object/utils.py:79), not seven — store.py:184 (_get_by_hfid, passes node_type) and ctl/object/utils.py:79 (passes node_type and branch_name) are omitted. node_type is therefore supplied by five sites, not three; identifier is a filter mapping at five store sites, not four. Re-count against the current sources so the justification for making the promoted attributes optional rests on accurate figures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At dev/specs/ifc-3034-error-catalogue/research.md, line 551:

<comment>The new table's counts of NodeNotFoundError client-side raise sites are inaccurate, and the optional-by-policy decision is justified on them. The repo actually has nine `raise NodeNotFoundError` sites (store.py:114/123/142/160/184, client.py:676/3273, file_handler.py:168, ctl/object/utils.py:79), not seven — store.py:184 (`_get_by_hfid`, passes `node_type`) and ctl/object/utils.py:79 (passes `node_type` and `branch_name`) are omitted. `node_type` is therefore supplied by five sites, not three; `identifier` is a filter mapping at five store sites, not four. Re-count against the current sources so the justification for making the promoted attributes optional rests on accurate figures.</comment>

<file context>
@@ -541,6 +541,50 @@ notes.
+| Attribute | Client-side raises | Catalogue payload |
+|-----------|--------------------|-------------------|
+| `identifier` | a filter mapping from four `store.py` sites and two `client.py` sites; a plain string from `file_handler.py:168` | a single string, declared required |
+| `node_type` | supplied by three of seven raise sites; the four `store.py` sites omit it and fall back to `"unknown"` | `node_kind`, declared required |
+| `branch_name` | supplied by three of seven raise sites | **not in the payload at all** |
+
</file context>

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