# llmlint: ignore-file[contracts_have_one_source_or_a_drift_gate] Linear exposes its schema only through an authenticated, unversioned explorer, so an authoritative automated freshness check would require the credential the live lane holds, and that lane skips where no credential was ever expected — a fork pull request, and a contributor with no keys — so it could not be the deterministic offline gate this rule asks for; this documentation-derived subset pins the acceptance criterion's recorded 2026-08-24 contract instead.
# Documentation-derived Linear GraphQL schema subset, pinned 2026-08-24, with
# `projectDelete` re-observed and pinned 2026-08-29 from the same published schema, and
# the whole document contract below — `document`, `documents`, `Document`,
# `DocumentConnection`, `DocumentFilter`, the two document inputs, both document payloads
# and the three document mutations — re-observed and pinned 2026-09-01 from it.
#
# `Query.projectStatuses` is corrected on 2026-09-04 from the real API's own answer, which
# is the only authority this file ever gets that outranks the published documentation: the
# live journey sent it a `filter` argument and Linear refused the whole document with
# `Unknown argument "filter" on field "Query.projectStatuses". Did you mean "after"?`. So
# that connection is not filterable and `ProjectStatusFilter`, which nothing now sends, is
# gone with it; the plugin matches the status name locally instead.
#
# `ProjectRelationCreateInput` is re-observed and pinned 2026-09-04 from the published
# schema, which now declares `anchorType: String!` and `relatedAnchorType: String!`
# alongside the three fields this file carried before. Linear added them under both `main`
# and this branch, and the live journey's project-relation write failed on the first with
# `Field "anchorType" of required type "String!" was not provided.` Both are bare `String!`
# there and Linear enumerates their values nowhere introspection reaches; `start`, `end`
# and `milestone` are the values, and the two this source sends are the whole-project
# anchors. The optional `id`, `projectMilestoneId` and `relatedProjectMilestoneId` that
# input also declares stay out, as every unsent field does.
#
# Those three values, and the one value `type` takes, were confirmed on 2026-09-04 from
# Linear's validator rather than derived: refusing an undocumented value it answers
# `anchorType must be one of the following values: start, end, milestone` and `type must be
# one of the following values: dependency`. Neither enumeration can be pinned as a GraphQL
# enum here, because Linear does not declare one — the fields really are `String!`, and the
# check that validates this file against the plugin's documents can only hold them to that.
# What holds the values is the plugin's own tests; `README.md` records the whole probe,
# including which pair of anchors means which direction.
#
# The team side of `WorkflowStateFilter` is corrected on 2026-09-04 from the real API's own
# answer, which is the second time that authority has outranked the documentation this file
# is otherwise derived from. This file carried the filter as an invented `IdComparator {
# id: IdEquality { eq: String } }`, and the plugin's state lookup declared `$team: String!`
# to match it. Linear refused the whole document with HTTP 400 and
# `Variable "$team" of type "String!" used in position expecting type "ID".`, which names
# both the real shape and why `String!` cannot stand there: a variable is usable at a
# location only when its type is the location's type or that type's non-null form, and
# `String` is not `ID`. So the real names and the real scalar are pinned —
# `WorkflowStateFilter.team` is a `NullableTeamFilter`, whose `id` is an `IDComparator`,
# whose `eq` is an `ID` — and `ISSUE_STATE` declares `$team: ID!`, which is the non-null
# form of the location's own type and is therefore accepted.
# Only the read/write contract exercised by this plugin is retained.
type Query {
viewer: User!
issue(id: String!): Issue
issues(first: Int!, after: String, filter: IssueFilter): IssueConnection!
project(id: String!): Project
projects(first: Int!, after: String, filter: ProjectFilter): ProjectConnection!
document(id: String!): Document!
documents(first: Int, after: String, filter: DocumentFilter): DocumentConnection!
issueLabels(first: Int, after: String, filter: IssueLabelFilter): IssueLabelConnection!
projectLabels(filter: ProjectLabelFilter): ProjectLabelConnection!
teams(filter: TeamFilter): TeamConnection!
workflowStates(filter: WorkflowStateFilter): WorkflowStateConnection!
projectStatuses(after: String): ProjectStatusConnection!
}
type Mutation {
issueCreate(input: IssueCreateInput!): IssuePayload!
issueUpdate(id: String!, input: IssueUpdateInput!): IssuePayload!
projectCreate(input: ProjectCreateInput!): ProjectPayload!
projectUpdate(id: String!, input: ProjectUpdateInput!): ProjectPayload!
issueRelationCreate(input: IssueRelationCreateInput!): IssueRelationPayload!
projectRelationCreate(input: ProjectRelationCreateInput!): ProjectRelationPayload!
issueRelationDelete(id: String!): DeletePayload!
projectRelationDelete(id: String!): DeletePayload!
issueDelete(id: String!): DeletePayload!
projectDelete(id: String!): ProjectArchivePayload!
documentCreate(input: DocumentCreateInput!): DocumentPayload!
documentUpdate(id: String!, input: DocumentUpdateInput!): DocumentPayload!
documentDelete(id: String!): DocumentArchivePayload!
}
type User {
id: ID!
}
type Issue {
id: ID!
title: String!
description: String
url: String
createdAt: DateTime
updatedAt: DateTime
archivedAt: DateTime
state: WorkflowState!
labels: IssueLabelConnection!
project: Project
relations(first: Int!, after: String): IssueRelationConnection!
inverseRelations(first: Int!, after: String): IssueRelationConnection!
}
type Project {
id: ID!
name: String!
description: String
url: String
createdAt: DateTime
updatedAt: DateTime
archivedAt: DateTime
status: ProjectStatus!
labels: ProjectLabelConnection!
relations(first: Int!, after: String): ProjectRelationConnection!
inverseRelations(first: Int!, after: String): ProjectRelationConnection!
}
# Linear's own first-class document. It carries no `labels` field and no status: the only
# types of the published schema with `labels` are Issue, Project, Team, Initiative and
# Organization, which is why this source reports a document's labels as none rather than
# standing a slot up beside a first-class type.
type Document {
id: ID!
title: String!
content: String
url: String!
createdAt: DateTime!
updatedAt: DateTime!
archivedAt: DateTime
project: Project
}
type WorkflowState {
id: ID!
name: String!
type: String!
}
type ProjectStatus {
id: ID!
name: String!
type: String!
}
type IssueLabel {
id: ID!
name: String!
color: String
}
type ProjectLabel {
id: ID!
name: String!
color: String
}
type IssueConnection {
nodes: [Issue!]!
pageInfo: PageInfo!
}
type ProjectConnection {
nodes: [Project!]!
pageInfo: PageInfo!
}
type IssueLabelConnection {
nodes: [IssueLabel!]!
pageInfo: PageInfo!
}
type ProjectLabelConnection {
nodes: [ProjectLabel!]!
pageInfo: PageInfo!
}
type DocumentConnection {
nodes: [Document!]!
pageInfo: PageInfo!
}
type IssueRelationConnection {
nodes: [IssueRelation!]!
pageInfo: PageInfo!
}
type ProjectRelationConnection {
nodes: [ProjectRelation!]!
pageInfo: PageInfo!
}
type IssueRelation {
id: ID!
type: String!
issue: Issue
relatedIssue: Issue
}
type ProjectRelation {
id: ID!
type: String!
project: Project
relatedProject: Project
}
type Team {
id: ID!
}
type TeamConnection {
nodes: [Team!]!
}
type WorkflowStateConnection {
nodes: [WorkflowState!]!
}
type ProjectStatusConnection {
nodes: [ProjectStatus!]!
}
type IssuePayload {
success: Boolean!
issue: Issue
}
type ProjectPayload {
success: Boolean!
project: Project
}
type IssueRelationPayload {
success: Boolean!
issueRelation: IssueRelation
}
type ProjectRelationPayload {
success: Boolean!
projectRelation: ProjectRelation
}
type DocumentPayload {
success: Boolean!
document: Document!
}
type DeletePayload {
success: Boolean!
}
type DocumentArchivePayload {
success: Boolean!
}
type ProjectArchivePayload {
success: Boolean!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
# `and` and `or` are members of every one of Linear's filter inputs, and this source uses
# `or` to spell "carries at least one of these labels" — see `StringComparator` below for
# why that cannot be one list operator.
# The two read filters below carried nothing but `and`/`or` until 2026-09-04, and that
# emptiness is what let two wrong members reach `projects(filter:)` for months: a filter is
# built at runtime and passed whole as `$filter`, so it appears nowhere in a document and
# the two checks that read documents could not see it. They are pinned member by member
# now, from Linear's real schema, and `every_variables_object_this_source_sends_conforms_to_the_pinned_schema`
# walks the objects this source really builds against them.
#
# **`IssueFilter` and `ProjectFilter` are different types, and the members they do not
# share are the point.** A project has no `team` — Linear refused one with
# `Field "team" is not defined by type "ProjectFilter". Did you mean "lead"?` — it has the
# teams it is accessible from, under `accessibleTeams`. And a project's status is not an
# issue's state: `ProjectFilter.state` exists and is a bare `StringComparator` over
# something else, while the counterpart of `IssueFilter.state` is `ProjectFilter.status`.
input IssueFilter {
team: TeamFilter
labels: IssueLabelCollectionFilter
state: WorkflowStateFilter
project: NullableProjectFilter
and: [IssueFilter!]
or: [IssueFilter!]
}
# `state` is the one member here this source does not send, and it is pinned because it is
# the trap: it exists, it is spelled exactly like the member of `IssueFilter` that carries
# an issue's status, and it is a bare `StringComparator` over something else entirely. With
# it absent, sending the issue's `{state:{type:{in:[…]}}}` here would be reported as a
# member Linear does not have, which is untrue; with it present the report is what is
# actually wrong — a `StringComparator` has no `type`.
input ProjectFilter {
id: EntityIdentifierIDComparator
accessibleTeams: TeamCollectionFilter
labels: ProjectLabelCollectionFilter
state: StringComparator
status: ProjectStatusFilter
and: [ProjectFilter!]
or: [ProjectFilter!]
}
# `DocumentFilter.project` is a `ProjectFilter` rather than the `NullableProjectFilter`
# `IssueFilter.project` is, so it carries no `null:` member and Linear cannot be asked for
# the documents belonging to no project. That predicate is the one this source applies to
# a fetched page itself.
input DocumentFilter {
project: ProjectFilter
and: [DocumentFilter!]
}
# `null:` is the whole reason `IssueFilter.project` reaches this rather than `ProjectFilter`:
# it is what asks for the issues belonging to no project at all.
input NullableProjectFilter {
id: EntityIdentifierIDComparator
null: Boolean
and: [NullableProjectFilter!]
or: [NullableProjectFilter!]
}
input TeamCollectionFilter {
some: TeamFilter
every: TeamFilter
}
input TeamFilter {
key: StringComparator
}
input IssueLabelCollectionFilter {
some: IssueLabelFilter
every: IssueLabelFilter
}
# Not a mirror of the collection above, and the difference is Linear's: `some` here takes
# the *collection* filter again where the issue side takes the plain one. Both carry the
# `name` this source sends, so one spelling satisfies each, which is why it is written once
# and pinned twice.
input ProjectLabelCollectionFilter {
name: StringComparator
some: ProjectLabelCollectionFilter
every: ProjectLabelFilter
}
input WorkflowStateFilter {
name: StringComparator
type: StringComparator
team: NullableTeamFilter
}
# The project counterpart of `WorkflowStateFilter`, and its `type` is a `StringComparator`
# although the `ProjectStatus.type` field it filters is the `ProjectStatusType` enum —
# `backlog`, `planned`, `started`, `paused`, `completed`, `canceled`. Neither `planned` nor
# `paused` is a workflow-state type and neither `unstarted` nor `triage` is a project-status
# one, so the two vocabularies are not interchangeable however alike the two filters look.
input ProjectStatusFilter {
type: StringComparator
}
input ProjectLabelFilter {
name: StringComparator
}
input IssueLabelFilter {
name: StringComparator
}
# Every member here is one this source sends. What is recorded rather than sent is the
# absence: `StringComparator` has no case-insensitive *list* operator. Linear said so
# itself, refusing `inIgnoreCase` with HTTP 400 — `Field "inIgnoreCase" is not defined by
# type "StringComparator". Did you mean "eqIgnoreCase" or "neqIgnoreCase"?` — which is
# where the two case-insensitive members below come from. `in` is the case-*sensitive* list
# operator, which is why the two status narrowings use it over a fixed vocabulary and the
# label predicates, over names a user typed, do not.
input StringComparator {
eqIgnoreCase: String
neqIgnoreCase: String
in: [String!]
}
input NullableTeamFilter {
id: IDComparator
}
input IDComparator {
eq: ID
}
# A project id is compared with this rather than with `IDComparator`; the member this
# source sends is spelled the same in both, and the types are not.
input EntityIdentifierIDComparator {
eq: ID
}
input IssueCreateInput {
teamId: String!
title: String!
description: String
stateId: String!
labelIds: [String!]!
projectId: String
}
input IssueUpdateInput {
title: String!
description: String
stateId: String!
labelIds: [String!]!
projectId: String
}
input ProjectCreateInput {
teamIds: [String!]!
name: String!
description: String
statusId: String!
labelIds: [String!]!
}
input ProjectUpdateInput {
name: String!
description: String
statusId: String!
labelIds: [String!]!
}
# Nullability as published: `title` alone is required of a create, and every member of an
# update is optional.
input DocumentCreateInput {
title: String!
content: String
projectId: String
teamId: String
}
input DocumentUpdateInput {
title: String
content: String
projectId: String
}
input IssueRelationCreateInput {
issueId: String!
relatedIssueId: String!
type: String!
}
input ProjectRelationCreateInput {
projectId: String!
relatedProjectId: String!
type: String!
anchorType: String!
relatedAnchorType: String!
}
scalar DateTime