Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Specification Quality Checklist: Rename the misleading `has_schema_changes` branch field

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-07-30
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- The API field names (`has_schema_changes`, `schema_differs_from_default_branch`)
appear throughout the spec. These are treated as the user-facing API contract
being changed, not as implementation detail - the entire feature is a rename of
a publicly queryable field, so the names are the subject matter.
- Two decisions that were open in INFP-469 are resolved and recorded: the final
name (`schema_differs_from_default_branch`, chosen over the `has_schema_diverged`
alternative for naming its reference point - see the Overview) and the removal
version (1.14.0).
- SDK scope is explicitly excluded (OOS-001) and routed to a follow-up ticket, per
the decision to avoid coupling the SDK to the latest Infrahub release. A second
follow-up (OOS-005) tracks the actual 1.14.0 removal. Both follow-up tickets are
required to exist before this feature is considered done.
- A few code-level references (the `Branch` vs `InfrahubBranch` GraphQL types, the
backend model property, the unrelated `SchemaAnalyzer.has_schema_changes()`
method, SDK file paths) appear in the requirements and scope sections. These are
present deliberately as scoping guardrails - to state which of several
identically named things are in and out of scope - not as design of the
implementation. Without them an implementer could rename the wrong symbol or
miss one of the two GraphQL representations.
- The UI copy is changed, not merely re-pointed: the current "schema updated" /
"Has schema changes" wording is misleading in the same way the field name is, so
FR-006 requires clearer wording that still fits the existing badge/label layout.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Contract: Branch `schema_differs_from_default_branch` GraphQL field

The externally observable contract for this feature is the GraphQL schema. Both the legacy
`Branch` type and the `InfrahubBranch` type gain a new field; the old field is marked deprecated
on both. This contract is verified against the regenerated `schema/schema.graphql` and via
introspection.

## `Branch` (legacy query type)

Target shape after regeneration (`schema/schema.graphql`, Branch type ~L233-246):

```graphql
type Branch {
branched_from: String
created_at: String
description: String
graph_version: Int
has_schema_changes: Boolean @deprecated(reason: "Use schema_differs_from_default_branch instead. has_schema_changes is scheduled for removal in Infrahub 1.14.0.")
schema_differs_from_default_branch: Boolean
id: String!
is_default: Boolean
is_isolated: Boolean @deprecated(reason: "non isolated mode is not supported anymore")
name: String!
origin_branch: String
status: BranchStatus!
sync_with_git: Boolean
}
```

## `InfrahubBranch` (paginated query type)

Target shape after regeneration (`schema/schema.graphql`, InfrahubBranch type ~L8827-8839):

```graphql
type InfrahubBranch {
branched_from: NonRequiredStringValueField
created_at: String
description: NonRequiredStringValueField
graph_version: NonRequiredIntValueField
has_schema_changes: NonRequiredBooleanValueField @deprecated(reason: "Use schema_differs_from_default_branch instead. has_schema_changes is scheduled for removal in Infrahub 1.14.0.")
schema_differs_from_default_branch: NonRequiredBooleanValueField
id: String!
is_default: NonRequiredBooleanValueField
is_isolated: NonRequiredBooleanValueField @deprecated(reason: "non isolated mode is not supported anymore")
name: RequiredStringValueField!
origin_branch: NonRequiredStringValueField
status: StatusField!
sync_with_git: NonRequiredBooleanValueField
}
```

## Behavioral contract

| # | Given | When (query) | Then | Spec ref |
|---|-------|--------------|------|----------|
| C1 | branch schema == default | `schema_differs_from_default_branch` | `false` | FR-001, US1-1 |
| C2 | branch uploaded own schema change | `schema_differs_from_default_branch` | `true` | US1-2 |
| C3 | untouched branch, default changed after | `schema_differs_from_default_branch` | `true` | US1-3 |
| C4 | any branch state | both fields in one query | identical values | FR-002, US1-4, SC-001 |
| C5 | any branch state | `has_schema_changes` | unchanged value (no breakage) | FR-003, SC-004 |
| C6 | introspection | inspect `has_schema_changes` on both types | `@deprecated` present, reason names replacement + 1.14.0 | FR-004/005, SC-002 |
| C7 | default branch | either field | `false` | Edge cases |
| C8 | mutation (`BranchCreate`/`BranchRebase`) payload | request `schema_differs_from_default_branch` | resolves correctly | FR-001 |

## Verification

- Regenerate: `uv run invoke schema.generate-graphqlschema`; the diff against
`schema/schema.graphql` shows exactly the two added fields + two `@deprecated` annotations.
- Introspection query on `__type(name: "Branch")` and `__type(name: "InfrahubBranch")` returns
`isDeprecated: true` and the `deprecationReason` for `has_schema_changes` (C6).
- A GraphQL query requesting both fields on assorted branch states returns equal values (C4).

## Non-contract (must NOT change)

- The `has_schema_changes` value or resolution logic (OOS-003).
- `SchemaAnalyzer.has_schema_changes()` and its callers (OOS-004).
- `schema/openapi.json` (field is GraphQL-only).
63 changes: 63 additions & 0 deletions dev/specs/ifc-2281-schema-differs-from-default/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Phase 1 Data Model: Rename the misleading `has_schema_changes` branch field

This feature introduces no new persisted data and changes no database schema. The "model" here
is the in-memory `Branch` model property and its projection through the GraphQL and frontend type
layers. No Neo4j nodes, edges, or migrations are added or altered (Constitution I/II preserved).

## Entity: Branch (backend model)

`backend/infrahub/core/branch/models.py`

| Member | Type | Status | Notes |
|--------|------|--------|-------|
| `schema_differs_from_default_branch` | `bool` (property) | **New** | Holds the computation: `True` when `schema_hash.main` differs from the origin branch's `schema_hash.main`; `False` when either hash is missing. |
| `has_schema_changes` | `bool` (property) | **Retained, deprecated in API** | Rewritten to `return self.schema_differs_from_default_branch`. No behavior change. Kept for the deprecation window. |

Validation / invariants (unchanged, now asserted by parity):
- Default branch → `False` (cannot differ from itself; origin unresolved or equal hash).
- No `schema_hash` yet → `False`.
- Origin branch schema state unavailable → `False`.
- For every branch state, both properties return identical values (SC-001).

No state transitions. The value is derived on read from cached schema hashes; nothing is stored.

## Projection: GraphQL types

`backend/infrahub/graphql/types/branch.py`

| Type | Field | Shape | Status |
|------|-------|-------|--------|
| `BranchType` (`Branch`) | `schema_differs_from_default_branch` | `Boolean(required=False)` | **New** |
| `BranchType` (`Branch`) | `has_schema_changes` | `Boolean(required=False)` + `deprecation_reason` | **Deprecated** |
| `InfrahubBranch` | `schema_differs_from_default_branch` | `Field(NonRequiredBooleanValueField, required=False)` | **New** |
| `InfrahubBranch` | `has_schema_changes` | `Field(NonRequiredBooleanValueField, required=False)` + `deprecation_reason` | **Deprecated** |

Both graphene field names must match the model property name because fields resolve via
`getattr(branch, field_name)` inside `to_graphql_flat` / `to_graphql`. Mutation payloads
(`BranchCreate`, `BranchRebase`, `BranchValidate`, `BranchMerge`) reuse `BranchType` and therefore
expose the new field automatically.

Deprecation reason (single shared constant, applied to both types):

```text
Use schema_differs_from_default_branch instead. has_schema_changes is scheduled for removal in Infrahub 1.14.0.
```

## Projection: frontend types

`frontend/app/src/entities/branches/domain/model/branch.ts`

| Interface | Field | Status |
|-----------|-------|--------|
| `BranchListItem` | `schema_differs_from_default_branch?: boolean \| null` | replaces `has_schema_changes?` |
| `BranchDetail` | `schema_differs_from_default_branch?: boolean \| null` | replaces `has_schema_changes?` |

Generated types in `frontend/app/src/shared/api/graphql/generated/types.ts` (`Branch`,
`InfrahubBranch`) are regenerated by `pnpm codegen`; they will carry both fields (new + deprecated)
until the old field is removed at 1.14.0. Frontend hand-written code references only the new field.

## Out of scope (not part of this data model)

- `MergeSchemaAnalyzer.has_schema_changes()` (merge diff method) - unrelated, unchanged (OOS-004).
- SDK `Branch` model fields (OOS-001) - separate follow-up.
- The `schema_hash` computation itself (OOS-003) - unchanged.
134 changes: 134 additions & 0 deletions dev/specs/ifc-2281-schema-differs-from-default/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Implementation Plan: Rename the misleading `has_schema_changes` branch field

**Branch**: `schema-differs-from-default-ifc-2281` | **Date**: 2026-07-30 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `/specs/ifc-2281-schema-differs-from-default/spec.md`

## Summary

Introduce a clearly named branch field, `schema_differs_from_default_branch`, that
returns exactly the value the existing `has_schema_changes` field returns (whether a
branch's schema hash differs from its origin/default branch), deprecate the old field
with a machine-readable reason naming the replacement and the 1.14.0 removal version,
and migrate Infrahub's own backend and frontend consumers onto the new name. This is a
naming, deprecation, and first-party consumer-migration change: the underlying
divergence computation is unchanged, both fields coexist through the deprecation window,
and the SDK is deliberately deferred to a follow-up ticket.

Technical approach: add a delegating `schema_differs_from_default_branch` property to the
`Branch` model (old property kept, delegating to it), expose it as a new graphene field
on both the legacy `Branch` type and the `InfrahubBranch` type (which resolve fields from
same-named model properties), add `deprecation_reason` to the existing `has_schema_changes`
graphene fields, repoint the two internal backend readers and all frontend queries/UI copy,
update tests for parity, regenerate `schema/schema.graphql` and the frontend generated types,
and add changelog fragments plus follow-up tracking tickets (SDK adoption, 1.14.0 removal).

## Technical Context

**Language/Version**: Python 3.14 (backend), TypeScript 5.9 / React 19.2 (frontend)

**Primary Dependencies**: graphene (GraphQL), FastAPI; frontend uses Apollo/urql generated
types via `pnpm codegen`

**Storage**: Neo4j. No schema, migration, or query changes - the field reads a value already
computed from cached schema hashes on the branch model.

**Testing**: pytest (backend unit/component/integration), Vitest (frontend unit), Playwright
(frontend E2E)

**Target Platform**: Linux server backend + web frontend

**Project Type**: Web application (backend + frontend), monorepo

**Performance Goals**: No change. The new field delegates to the existing property; no new
database access is introduced.

**Constraints**: Non-breaking during the deprecation window - `has_schema_changes` must keep
returning identical values for as long as both fields exist (SC-001, SC-004). Removal is
pinned to Infrahub 1.14.0 (against the current 1.11 dev line).

**Scale/Scope**: Small, well-bounded. One model property, two graphene types, four mutation
payload types (via shared `BranchType`), two internal backend readers, ~4 frontend GraphQL
operations, 2 UI copy strings, and their tests.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- **I. Schema-Driven Integrity**: PASS. Generated artifacts (`schema/schema.graphql`, frontend
`src/shared/api/graphql/generated/`) are regenerated via `uv run invoke schema.generate-graphqlschema`
and `cd frontend/app && pnpm codegen`, never hand-edited. No data-schema change.
- **II. Branch-Safe by Default**: PASS. No new queries; the underlying property is already
branch-aware and unchanged. No merge/rebase behavior change (the two internal readers in
`core/branch/tasks.py` keep identical semantics, only the property name they read changes).
- **III. Type Safety & Explicit Contracts**: PASS. New property is typed `-> bool`; the GraphQL
contract is defined before implementation (see `contracts/`); frontend consumes generated types.
- **IV. Test Discipline**: PASS with note. Existing backend GraphQL/lifecycle tests and frontend
fixtures are updated; a parity test asserting both fields agree is added. US3 is user-facing
(UI copy), so frontend unit/component coverage is updated; an E2E is added only if an existing
branch-view Playwright spec covers this indicator (no behavior change, copy + field source only).
- **V. Query Performance & Efficiency**: PASS. No new or modified queries.
- **VI. Security & Input Boundaries**: PASS. No new input boundary; read-only field.
- **VII. Simplicity & Maintainability**: PASS. Old property delegates to the new one (single
source of computation, no duplication). Follows the existing `is_isolated` deprecation pattern
rather than introducing new deprecation infrastructure.

No violations. Complexity Tracking not required.

## Project Structure

### Documentation (this feature)

```text
specs/ifc-2281-schema-differs-from-default/
├── plan.md # This file (/speckit-plan command output)
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
│ └── graphql-branch-field.md
├── checklists/
│ └── requirements.md # Pre-existing
└── tasks.md # Phase 2 output (/speckit-tasks - NOT created here)
```

### Source Code (repository root)

```text
backend/infrahub/
├── core/branch/
│ ├── models.py # Add schema_differs_from_default_branch property (delegates); keep has_schema_changes
│ └── tasks.py # Repoint the two internal readers (lines ~204, ~232) to the new property
└── graphql/
├── types/branch.py # Add new field on BranchType + InfrahubBranch; add deprecation_reason to has_schema_changes
├── queries/branch.py # No change expected (field selection is generic)
└── mutations/branch.py # No change expected (payloads use shared BranchType.to_graphql_flat)

frontend/app/src/entities/branches/
├── api/
│ ├── get-branches-from-api.ts # Request new field
│ ├── get-branch-details-from-api.ts # Request new field
│ ├── create-branch-from-api.ts # Request new field
│ ├── rebase-branch-from-api.ts # Request new field
│ └── branch.mappers.ts # Map new field
├── domain/model/branch.ts # Rename field on BranchListItem / BranchDetail
├── domain/use-cases/create-branch.ts # Default value key rename
└── ui/
├── branch-list-item/branch-schema-changes-badge.tsx # Copy: "schema updated" -> differs-from-default wording
├── branch-list-item/branch-list-item.tsx # Field reference
├── branches-table/cells/branch-name-cell.tsx # Field reference
├── branches-to-select-options.ts # Field reference
└── branch-details/branch-attributes.tsx # Copy: "Has schema changes" label -> differs-from-default wording

schema/schema.graphql # Regenerated (Branch + InfrahubBranch types)
frontend/app/src/shared/api/graphql/generated/ # Regenerated (types.ts et al.)
changelog/ # +schema-differs-from-default-branch.added.md, .deprecated.md
```

**Structure Decision**: Web application (Option 2). The change threads through the existing
backend branch model + GraphQL layer and the frontend `entities/branches` feature slice; no new
directories or modules are introduced.

## Complexity Tracking

> No Constitution Check violations. Section intentionally empty.
Loading
Loading