onetaskgraph-github-projects 0.2.16

A onetaskgraph source over GitHub Projects.
Documentation

A stateless onetaskgraph source over one GitHub Projects v2 board.

A board is a container of projects, not a project. Its own title, shortDescription and readme are never read as an item's fields and are never written: nothing in this source can rename the board a user configured.

A project is an issue and its tasks are that issue's sub-issues. GitHub's schema decides that: Issue exposes parent, subIssues and subIssuesSummary, and DraftIssue exposes none of them. Creating an issue needs a repositoryId, and a board has none, so [GitHubProjectsConfig::repository] names the one repository this source creates its project and task issues in; a write without it is refused naming the field.

Telling a project from a task. A board issue is a project when either it has sub-issues or it carries [ItemKind::METADATA_KEY]; otherwise it is a task. A sub-issue is always a task, whatever it carries. The marker is sufficient and never necessary: it is what makes an empty project — the state a project copy passes through between creating the project and filing its first task — readable as a project, while the sub-issue arm lets a person author a project on the board by hand with no knowledge of this product's metadata at all. Pull requests are neither a project nor a task and are ignored.

Where metadata lives. Short typed things go to typed fields and native relations: status to the board's Status single-select and the issue's own state, the copy origin to a source-owned onetaskgraph.origin text field, and dependencies to blockedBy and to sub-issue links. Unbounded caller JSON goes in a trailing <!-- onetaskgraph.metadata ... --> comment at the end of the issue body — the same encoding docs/metadata.md settles for Linear, not a second one. A ProjectV2 text field is length-bounded and shortDescription is capped at 300 characters, which is why neither can hold a caller's own prose.

Status. status_mapping is per-instance configuration from a status category to null, a board Status option name, or a closed state of completed or not-planned. Nothing here ever calls updateProjectV2Field: that mutation's singleSelectOptions overwrites a field's option set, so no addition is additive and a mistake destroys every item's status. A status this board cannot represent is a refusal naming the status and the instance instead.

done closes the issue by default because GitHub derives subIssuesSummary.completed and the board's own Sub-issues progress field from closed sub-issues: a plan whose finished tasks were only moved to a "Done" column would read 0% complete forever.

What this source declares, field by field

One verdict per field of [Capabilities], and what Native means when this source says it. Proven means a shared journey drives it against the real binary over this source's own row in crates/onetaskgraph/tests/e2e/fixtures.rs, and every_row_declares_exactly_what_its_plugin_reports is what keeps this list and capabilities from parting.

Field Verdict
projects Supported and proven. A task's project is the issue it is a sub-issue of, and a listing scoped to one keeps the items filed under that issue. This is the field that was declared and then not applied, which silently returned another project's tasks.
documents Unsupported, and unimplemented. A board holds issues, and this source reads them as tasks and as projects; nothing here reads a document. A repository holds files a board item could name, so this is work nobody has written rather than something the backend forbids; docs/follow-ups.md tracks it.
orphan_tasks Supported and proven. A task issue with no parent is in no project.
filter_by_label Supported and proven, over the issue's own labels.
filter_by_status Supported and proven, over the board's Status option and the issue's open or closed state, through this instance's own status_mapping.
search_title Supported and proven, over Issue.title.
search_content Supported and proven, over the visible body — the trailing metadata comment is not part of it.
task_dependencies Supported and proven, in both directions: blockedBy and blocking.
project_dependencies Supported and proven, in both directions, over the same two connections, because a project here is an issue.
max_page_size Supported and proven. [MAX_PAGE_SIZE], GitHub's own connection maximum.

No predicate here is unsupported — documents is the one unsupported field, and it is not a predicate but a statement that this source has no documents at all — and the three facts behind the uniform Native are recorded below rather than re-derived, because a reader who takes Native to mean the remote service filters will read that uniformity as a lie.

First, the plugin contract defines Support::Native as the source applies this predicate itself, and says nothing about where it applies it. What the declaration promises the engine is capability rule 1 — a predicate declared Native is applied — so that the engine may push it down and apply nothing of its own.

Second, this source can keep that promise for every predicate at no additional API cost, because this source's own board walk already reads every page of the board before it answers anything at all. That walk is what get_task, labels and every listing already pay for; filtering the items it returns is in-process work over data already in hand.

Third, no predicate could be pushed into the API even if that were wanted: ProjectV2.items takes first and after and offers no filter argument of any kind. So there is no server-side filtering to declare, and — the other half of the same fact — there is no predicate here that is genuinely unsupportable. Declaring one Unsupported would make the engine compensate for work this source has already done, and declaring projects native while ignoring the filter (which this source once did) silently returns another project's tasks, because the engine trusts the declaration and applies nothing locally.

Filtering happens before paging, so a page of a filtered result is a page of the survivors rather than the survivors of a page. Label and text matching answer the same question the same way the local Markdown source's do, so one cross-source expectation holds for both.

Required checks use only the local fixture server; the ignored credentialed lane verifies the current schema, then drives every field of the table above against the real board. It builds its own fixture there — two projects, one task filed under each, one filed under neither, a label on one of the three and a closed status on another — because that shape is what tells an honoured predicate from an ignored one: a board holding a single project answers a project filter the same way whether or not this source applies it, which is exactly how the defect above went unseen.

That lane writes only to the board GH_PROJECTS_OWNER and GH_PROJECTS_NUMBER name, and only into the repository GH_PROJECTS_REPOSITORY names, and skips — as it does without GH_PROJECTS_TOKEN — when any of them is absent. Requiring both to be nominated is what keeps a credentialed write lane off a board and a repository nobody nominated; it never asks GitHub which project was updated most recently. Before it starts, the lane also clears any item titled — and any repository label named — the way it titles and names its own artifacts, which is self-healing after an interrupted run: a process killed between its writes and its cleanup leaves artifacts the next run removes.