|
| 1 | += {my-title} |
| 2 | +Tj Vanderpoel (bougyman) <tj.vanderpoel@prizepicks.com> |
| 3 | +:revdate: Sep 04, 2026 |
| 4 | +:my-title: Phase 16 plan: issue relationship support |
| 5 | +:icons: font |
| 6 | +:env-github: |
| 7 | +ifdef::env-github[] |
| 8 | +:tip-caption: :bulb: |
| 9 | +:note-caption: :information_source: |
| 10 | +:important-caption: :heavy_exclamation_mark: |
| 11 | +:caution-caption: :fire: |
| 12 | +:warning-caption: :warning: |
| 13 | +endif::[] |
| 14 | +:toc: |
| 15 | + |
| 16 | +== Goal |
| 17 | + |
| 18 | +Make issue relationships first-class in `lc` so users can inspect and manage |
| 19 | +dependencies without leaving the terminal. In particular, a planner must be |
| 20 | +able to express that one issue is blocked by another and verify the resulting |
| 21 | +Linear dependency link. |
| 22 | + |
| 23 | +== Context |
| 24 | + |
| 25 | +`mix lc issue create` and `mix lc issue update` currently have no relationship |
| 26 | +operation. The documented ordering of related work therefore remains prose in |
| 27 | +issue descriptions rather than a real Linear dependency graph. |
| 28 | + |
| 29 | +Linear's checked-in GraphQL schema already provides the necessary API: |
| 30 | + |
| 31 | +* `Issue.relations` and `Issue.inverseRelations` for outgoing and incoming |
| 32 | + links; |
| 33 | +* `issueRelationCreate`, `issueRelationDelete`, and `issueRelationUpdate` |
| 34 | + mutations; and |
| 35 | +* the `blocks`, `related`, and `duplicate` relation types. |
| 36 | + |
| 37 | +The source endpoint of a `blocks` relation blocks the related endpoint. That |
| 38 | +wire-level direction must not force users to mentally reverse a normal |
| 39 | +dependency statement. |
| 40 | + |
| 41 | +== Command design |
| 42 | + |
| 43 | +Add an `issue relation` command group with three initial leaf commands: |
| 44 | + |
| 45 | +[source,sh] |
| 46 | +---- |
| 47 | +# Show both directions for EXT-38. |
| 48 | +lc issue relation list EXT-38 |
| 49 | +
|
| 50 | +# State the dependency in the natural direction: EXT-38 is blocked by EXT-37. |
| 51 | +lc issue relation add EXT-38 EXT-37 --type blocked-by |
| 52 | +
|
| 53 | +# One issue may be blocked by any number of issues in one command. |
| 54 | +lc issue relation add EXT-39 EXT-37 EXT-38 --type blocked-by |
| 55 | +
|
| 56 | +# Remove one or more relationships using the same natural statement. |
| 57 | +lc issue relation remove EXT-39 EXT-37 EXT-38 --type blocked-by |
| 58 | +---- |
| 59 | + |
| 60 | +`list` has the `ls` alias. The first positional argument is always the subject |
| 61 | +of the statement and every following `RELATED_ISSUE` is an independent other |
| 62 | +issue; command order is therefore never inferred from a relation ID or the |
| 63 | +order returned by Linear. `add` and `remove` require one subject plus one or |
| 64 | +more related issues. |
| 65 | + |
| 66 | +[cols="1,3", options="header"] |
| 67 | +|=== |
| 68 | +| Command |
| 69 | +| Meaning |
| 70 | + |
| 71 | +| `lc issue relation add A B... --type blocks` |
| 72 | +| A blocks every named B. |
| 73 | + |
| 74 | +| `lc issue relation add A B... --type blocked-by` |
| 75 | +| A is blocked by every named B. The implementation reverses each endpoint |
| 76 | + pair and sends a GraphQL `blocks` relation from each B to A. |
| 77 | + |
| 78 | +| `lc issue relation add A B... --type related` |
| 79 | +| A is related to every named B. |
| 80 | + |
| 81 | +| `lc issue relation add A B... --type duplicate` |
| 82 | +| A is a duplicate of every named B. |
| 83 | +|=== |
| 84 | + |
| 85 | +`--type` is required and accepts exactly `blocks`, `blocked-by`, `related`, |
| 86 | +and `duplicate`. `blocked-by` is a CLI convenience, not a GraphQL type. |
| 87 | + |
| 88 | +`list` presents outbound and inbound relations separately, with the related |
| 89 | +issue identifier and title, relation type in the user's direction, and the |
| 90 | +relation ID. Human output must say `Blocked by` for an incoming `blocks` |
| 91 | +relation and `Blocks` for an outgoing one. JSON output must retain the API |
| 92 | +relation ID, source issue, related issue, wire type, and direction so scripts |
| 93 | +do not need to parse terminal prose. The human output must have distinct |
| 94 | +`Blocks` and `Blocked by` sections; each line names the other issue, so a user |
| 95 | +can see the direction without reconstructing it from argument order. |
| 96 | + |
| 97 | +`add` and `remove` are idempotent per target from the user's perspective: |
| 98 | +adding an already-present identical relation succeeds with an informative |
| 99 | +no-change result; removing an absent relation does the same. A multi-target |
| 100 | +operation attempts every valid target and reports a result for each one; it |
| 101 | +does not roll back successful independent mutations because Linear supplies no |
| 102 | +transaction spanning relations. Its non-zero exit status and machine-readable |
| 103 | +output must identify every failed target. Reject a self-link and ambiguous or |
| 104 | +unknown relation type values before making that target's API request. Do not |
| 105 | +add a generic relation-update command in this phase; a type change is an |
| 106 | +explicit remove/add operation until its user-facing semantics are designed. |
| 107 | + |
| 108 | +== Domain and API design |
| 109 | + |
| 110 | +Add `LinearCli.Linear.IssueRelation` as an Ash resource registered in |
| 111 | +`LinearCli.Linear`. It has manual read/create/destroy actions backed directly |
| 112 | +by `LinearCli.Api`; no Ecto data layer and no Ash DSL relationship declaration |
| 113 | +is introduced. |
| 114 | + |
| 115 | +Expose named domain code interfaces for listing an issue's relations, creating |
| 116 | +one, and deleting one. The CLI layer resolves human issue identifiers through |
| 117 | +the existing issue lookup helpers, normalizes the directional `--type` value, |
| 118 | +and calls those interfaces rather than constructing GraphQL documents itself. |
| 119 | + |
| 120 | +The GraphQL selection must retrieve enough data to render and safely remove a |
| 121 | +relation: relation ID and type plus both endpoint issues' IDs, identifiers, |
| 122 | +titles, and URLs. Read both `relations` and `inverseRelations`, handle their |
| 123 | +pagination, and preserve the API's error response rather than treating an |
| 124 | +authorization or validation error as an empty list. |
| 125 | + |
| 126 | +For deletion, first resolve the exact relation from the two supplied issues and |
| 127 | +normalized type. If Linear returns duplicate rows for an impossible/legacy |
| 128 | +state, fail with a diagnostic that names every matching relation ID instead of |
| 129 | +deleting arbitrarily. Treat Linear's normal duplicate/invalid mutation errors |
| 130 | +as actionable CLI errors. Do not attempt to infer or alter parent/sub-issue |
| 131 | +hierarchies; they are distinct from issue relations. |
| 132 | + |
| 133 | +== CLI integration and display |
| 134 | + |
| 135 | +Extend the Optimus command specification, dispatch, aliases, and help text for |
| 136 | +the `issue relation` group, including `lc issue relation ls ISSUE` as the |
| 137 | +documented alias for `list`. Follow existing `issue view` lookup behavior for |
| 138 | +issue identifiers, including expansion of a bare numeric ID where that helper |
| 139 | +supports it. The help synopsis must name the arguments `ISSUE` and |
| 140 | +`RELATED_ISSUE...` and include the directional table above. |
| 141 | + |
| 142 | +Enhance full `issue view` output to show relationships through the same display |
| 143 | +formatter used by `issue relation list`; compact issue lists remain unchanged. |
| 144 | +The explicit `issue relation list` command is still required for discoverable, |
| 145 | +scriptable relationship inspection. |
| 146 | + |
| 147 | +Successful mutations print the created or removed relationship in human and |
| 148 | +JSON output. Do not use a generic success sentence that hides the direction; |
| 149 | +the output must make it obvious whether the named issue now blocks or is |
| 150 | +blocked by the other one. |
| 151 | + |
| 152 | +== Tests, documentation, and safety |
| 153 | + |
| 154 | +Add unit tests at each boundary: |
| 155 | + |
| 156 | +* resource/manual-action tests for GraphQL documents, variables, response |
| 157 | + mapping, pagination, and API error propagation; |
| 158 | +* domain code-interface tests; |
| 159 | +* CLI/parser tests for every command, aliases, required `--type`, one-or-more |
| 160 | + related issues, invalid types, self-links, reversed `blocked-by` variables, |
| 161 | + per-target idempotency/partial failure, and ambiguous deletion; and |
| 162 | +* display tests for both directions and JSON's stable relation fields. |
| 163 | + |
| 164 | +Update `documents/ash-domain-erd.adoc` in the same change to add the resource, |
| 165 | +its public attributes, code interfaces, nested endpoint associations, and the |
| 166 | +explicit statement that these remain GraphQL/nested associations rather than |
| 167 | +Ash DSL relationships. Update the README's development/CLI usage material and |
| 168 | +any command reference or completion tests. |
| 169 | + |
| 170 | +Use only `mix lc` for Linear interaction in repository documentation and agent |
| 171 | +workflows. Verify against a disposable pair of test issues: create every |
| 172 | +supported type, list from both endpoints, remove it, and confirm that repeated |
| 173 | +add/remove operations have the documented idempotent result. |
| 174 | + |
| 175 | +== Acceptance criteria |
| 176 | + |
| 177 | +* `lc issue relation list` (and `ls`), `add`, and `remove` are discoverable in |
| 178 | + `--help` and work with human and JSON output. |
| 179 | +* `--type blocked-by` creates and removes the correctly reversed GraphQL |
| 180 | + `blocks` relation. |
| 181 | +* One `add` or `remove` command accepts an arbitrary non-empty list of related |
| 182 | + issues and reports a separate outcome for every target. |
| 183 | +* Both endpoints show the same relationship in their natural direction. |
| 184 | +* Duplicate adds and absent removes are safe no-ops; self-links and ambiguity |
| 185 | + fail before unintended mutation. |
| 186 | +* The domain resource, code interfaces, CLI tests, display tests, README, and |
| 187 | + Ash domain ERD remain in sync. |
| 188 | +* No raw GraphQL or alternate Linear client is introduced outside |
| 189 | + `LinearCli.Api`. |
0 commit comments