Skip to content

Commit aa03320

Browse files
authored
docs(planning): adds phase-17 for showing labels in lc issue list/view (#257)
1 parent e1551ef commit aa03320

1 file changed

Lines changed: 311 additions & 0 deletions

File tree

‎documents/phase-17-plan.adoc‎

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
= {my-title}
2+
Tj Vanderpoel (bougyman) <bougyman@users.noreply.github.com>
3+
:revdate: Sep 07, 2026
4+
:my-title: Phase 17 plan: separate issue-list label display from label filtering
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+
Give issue listing an explicit presentation flag. `-i/--include-labels`
19+
includes each issue's labels in the output without selecting a subset of
20+
issues. Keep the existing `-l/--labels NAME[,NAME...]` spelling for label-name
21+
filtering and `-s/--status STATUS` spelling for friendly-status filtering.
22+
Every existing option keeps its contract; this phase adds only the missing
23+
presentation control:
24+
25+
[source,sh]
26+
----
27+
# Include labels on the normal compact listing without filtering its issues.
28+
lcls --include-labels
29+
lcls -i
30+
31+
# Include every assignee and every lifecycle state, with labels in the output.
32+
lcls -N --include-labels --all
33+
lcls -N -i --all
34+
35+
# Return issues carrying Bug, and include their labels in the output.
36+
lcls --labels Bug
37+
lcls -l Bug
38+
39+
# Match Bug or Feature, case-insensitively.
40+
lcls --labels Bug,Feature
41+
42+
# Show labels for a specifically named issue.
43+
lcls CRY-123 --include-labels
44+
45+
# Existing status filtering composes without changing its short option.
46+
lcls -s "Human Review" --include-labels
47+
----
48+
49+
== Context and failure mode
50+
51+
Versions 2.7.0 and 2.8.0 expose `-l/--labels LABELS` on `issue list` as a
52+
value-taking filter. Commit `f966f14` introduced the filter; commit `8073f9a`
53+
then made compact label rendering conditional on that same filter being
54+
present. That coupled three different concerns:
55+
56+
* which issues the API selects;
57+
* whether the GraphQL selection retrieves each issue's labels; and
58+
* whether the CLI renders those labels.
59+
60+
The coupling produces a silent and misleading failure for the motivating
61+
command:
62+
63+
[source,sh]
64+
----
65+
lcls -N --labels --all
66+
----
67+
68+
Optimus treats `--labels` as an option requiring a value and consumes `--all`
69+
as that value. `parse_labels/1` accepts it as the label name `"--all"`, so the
70+
real `--all` flag is never set. The CLI sends a case-insensitive filter for a
71+
label literally named `--all`; Linear returns no issues; formatting the empty
72+
list produces only a newline and exit status zero. The user's real labels are
73+
irrelevant because the request accidentally names a label that does not exist.
74+
75+
The behavior was reproduced against the installed 2.8.0 binary. Supplying the
76+
intended value (`lcls -N --labels Bug --all`) returned the existing Bug-labeled
77+
issues, confirming that authentication, label data, and the Linear API were
78+
all working.
79+
80+
Phase 17 does not reinterpret that malformed command as presentation. It gives
81+
presentation the explicit `--include-labels` name and makes the malformed
82+
`--labels --all` form fail clearly. The corrected presentation command is
83+
`lcls -N --include-labels --all`.
84+
85+
== Decisions
86+
87+
1. *`-i/--include-labels` is the boolean issue-list display flag.* It requests
88+
label data and includes every returned issue's labels in text and JSON
89+
output. It never adds a label filter. In compact text, labels remain the
90+
existing trailing bracketed list (`[Bug, Feature]`); an issue with no labels
91+
gets no empty bracket marker. `-i` is unused by `issue list` and by the
92+
inherited global options; the top-level bare `i` subcommand alias is a
93+
different token and does not conflict.
94+
2. *`-l/--labels` remains the issue-list label filter.* It takes one label name
95+
or a comma-separated list and preserves the existing case-insensitive OR
96+
semantics. A label filter continues to imply label display so a user can see
97+
all labels on every match without also spelling `--include-labels`.
98+
3. *Presentation and filtering compose but do not imply each other in the
99+
other direction.* `--include-labels` alone changes only fetched/rendered
100+
fields; `--labels Bug` filters and displays; using both is valid and
101+
equivalent to the filter alone for label retrieval and rendering.
102+
4. *`-s/--status` stays exactly as it is.* Reusing `-s` for presentation would
103+
break an established issue-list contract, so it is explicitly out of scope.
104+
`-l/--labels`, `-s/--status`, and every other existing short and long option
105+
retain their current spelling and meaning.
106+
5. *An option token must not silently become a label-filter value.* The
107+
`--labels` parser must reject a following recognized option such as `--all`
108+
with a clear missing-value/invalid-value error before any API call. Moving
109+
presentation to a new flag without closing the original parser hole would
110+
leave the motivating silent failure intact.
111+
6. *`issue create` is unchanged.* Its existing `-l/--labels Bug,Feature`
112+
option assigns labels to a new issue and remains value-taking. Optimus
113+
scopes options to their leaf subcommand; the create and list forms happen to
114+
retain the same value-taking labels spelling but have independent parsers.
115+
7. *Do not fetch label fields by default.* A normal list without
116+
`--include-labels` or `--labels VALUE` keeps the smaller GraphQL selection and
117+
existing compact output. The new behavior is opt-in and does not add
118+
avoidable response weight to every listing.
119+
120+
== Verified implementation boundaries
121+
122+
* `app/lib/linear_cli/cli.ex` defines `issue list`'s current
123+
`-l/--labels` entry under `options`, routed through `parse_labels/1`. The
124+
same list spec binds `-s` to `--status`; both existing entries stay intact.
125+
The separately scoped `issue create` labels option also does not change in
126+
this phase.
127+
* `LinearCli.CLI.Commands.issue_list/1` currently copies
128+
`options.labels` into the domain input and passes
129+
`labels: input.labels != []` to `Display.show/2`. This is the CLI-layer
130+
coupling point to split.
131+
* `LinearCli.Linear.Issue`'s `:list` action currently has one `labels` array
132+
argument, used as the server-side filter. `Issue.Read.List.read/4` also uses
133+
`args.labels != []` to choose between the base GraphQL document and the
134+
label-bearing document. The filter argument can stay intact; a separate
135+
`include_labels` boolean is needed for field selection.
136+
* `Issue.from_map/1` already parses `issue.labels.nodes` into `%Label{}`
137+
structs, and `Issue.list_fields_with_labels/0` already supplies the needed
138+
GraphQL selection. No new resource or response mapper is required.
139+
* `LinearCli.CLI.Display` already accepts a boolean `labels` display option and
140+
has tests for showing and hiding trailing label brackets. That internal
141+
contract and the current accessible, linear text format can remain intact.
142+
* ID-driven `issue list` calls use `Issue.full_fields/0`, which already fetches
143+
labels. `--include-labels` controls whether compact output prints them; the
144+
existing full-output `Labels:` line remains unchanged.
145+
146+
== Implementation design
147+
148+
=== CLI parsing
149+
150+
In `app/lib/linear_cli/cli.ex`:
151+
152+
* add `include_labels` to the `issue list` `flags` block with short name `-i`,
153+
long name `--include-labels`, and help text such as
154+
`Include labels in issue list output`;
155+
* retain the value-taking `labels` option as `-l/--labels LABELS` and preserve
156+
its comma-separated, case-insensitive OR parser;
157+
* retain the existing `-s/--status STATUS` option unchanged; and
158+
* make `parse_labels/1` reject a recognized flag token supplied as
159+
`--labels`' would-be value, so `lcls --labels --all` exits as a usage error
160+
and makes no GraphQL request.
161+
162+
Do not change the separately scoped `issue create` labels option or its
163+
parser.
164+
165+
=== Command and domain inputs
166+
167+
In `LinearCli.CLI.Commands.issue_list/1`:
168+
169+
* retain `label_filter = Map.get(options, :labels) || []`;
170+
* derive `include_labels` as
171+
`Map.get(flags, :include_labels, false) || label_filter != []`;
172+
* pass the existing domain filter as `labels: label_filter`;
173+
* pass the new domain selection switch as
174+
`include_labels: include_labels`; and
175+
* pass the existing `labels: include_labels` option to `Display.show/2`.
176+
177+
In the `LinearCli.Linear.Issue` `:list` action, add
178+
`argument :include_labels, :boolean, default: false`. Keep `:labels` as the
179+
array of names used by `maybe_put_label_filter/2`; this avoids an unnecessary
180+
break in the domain code interface while giving each concern one explicit
181+
input.
182+
183+
`Issue.Read.List` chooses `list_document_with_labels/0` from
184+
`args.include_labels`, not from whether `args.labels` is empty. Filtering still
185+
comes exclusively from `maybe_put_label_filter/2`. As a defensive domain
186+
invariant, a non-empty `args.labels` value should also force label field
187+
selection even if a future caller forgets to set `include_labels`; the CLI
188+
sets both deliberately, while the manual read remains safe for direct domain
189+
callers.
190+
191+
=== Display and output formats
192+
193+
Keep the internal boolean `labels` display option unchanged. Compact text
194+
continues to append all returned label names in brackets when enabled. It does
195+
not show only the name used to filter: an issue matching `Bug` but also carrying
196+
`Feature` renders both.
197+
198+
JSON output must contain the populated `labels` array whenever
199+
`--include-labels` or `--labels VALUE` requested label data. Without either
200+
switch, its current unloaded empty/default value remains unchanged; this phase
201+
does not make every JSON listing pay for nested label data.
202+
203+
== Tests
204+
205+
Extend the existing consolidated test files rather than introducing a new
206+
suite.
207+
208+
`app/test/linear_cli/cli/issue_commands_test.exs`:
209+
210+
* the desired command, `issue list -N --include-labels --all` (and its `-i`
211+
spelling), parses both boolean flags, sends neither an assignee nor
212+
lifecycle-date filter, sends no label filter, requests label fields, and
213+
renders returned labels;
214+
* bare `--include-labels` requests label fields while leaving the API filter
215+
free of a `labels` key;
216+
* `--labels Bug` and `-l Bug` preserve single-name case-insensitive filtering
217+
and display labels;
218+
* `--labels Bug,Feature` preserves OR filtering and displays every label on a
219+
matching issue;
220+
* `--labels no-such-label` returns the normal empty result;
221+
* `--labels` followed by `--all` is a clear usage error and sends no request;
222+
* `-s "Human Review"` and `--status "Human Review"` retain friendly-status
223+
filtering and compose with `-i/--include-labels`;
224+
* a positional issue identifier composed with trailing `--include-labels` still
225+
uses the full issue lookup and renders its labels;
226+
* with neither label switch, labels stay out of the compact GraphQL selection
227+
and text; and
228+
* `--output json --include-labels` contains the fetched label objects without
229+
human text mixed into stdout.
230+
231+
`app/test/linear_cli/linear/issue_test.exs`:
232+
233+
* `include_labels: true, labels: []` selects and parses label fields without
234+
adding a label filter;
235+
* `include_labels: false, labels: []` retains the base selection;
236+
* non-empty `labels` retains the existing single/multiple filter shapes and
237+
defensively selects label fields; and
238+
* API errors and pagination behave identically for both document variants.
239+
240+
`app/test/linear_cli/cli/display_test.exs` retains its existing boolean
241+
`labels` option coverage for multiple labels and an unlabeled issue; no display
242+
API rename is part of this phase.
243+
244+
The focused verification command is:
245+
246+
[source,sh]
247+
----
248+
cd app
249+
mix test \
250+
test/linear_cli/cli/issue_commands_test.exs \
251+
test/linear_cli/linear/issue_test.exs \
252+
test/linear_cli/cli/display_test.exs
253+
----
254+
255+
Finish with the repository's complete `mix precommit` gate from the root.
256+
257+
== Documentation
258+
259+
* `Readme.adoc`'s List Issues section gains the command contract shown in the
260+
Goal, including the explicit distinction between `--include-labels`
261+
(presentation) and `--labels` (filtering). No migration note is needed for
262+
existing valid commands because their contracts do not change.
263+
* Optimus-generated `issue list --help` must add `-i, --include-labels` under
264+
FLAGS while retaining `-l, --labels LABELS` and `-s, --status STATUS` under
265+
OPTIONS.
266+
* `documents/ash-domain-erd.adoc` must be updated in the implementation change
267+
because adding the `include_labels` Ash action argument and materially
268+
clarifying the `labels` argument changes the canonical domain inventory.
269+
* Release notes call out the additive `-i/--include-labels` flag. Do not edit
270+
historical changelog entries that accurately describe 2.7/2.8.
271+
* `AGENTS.md` indexes this Phase 17 plan. No repository-layout or module-tree
272+
update is needed because the phase adds no top-level directory or major
273+
architectural layer.
274+
275+
== Sequencing
276+
277+
This phase depends on the label-filter and compact-label work already released
278+
in 2.7.0 and 2.8.0 and is based on `origin/main` at v2.8.0.
279+
280+
1. Change the `issue list` Optimus contract and add parser-level regression
281+
coverage, including `-N --include-labels --all`, `-i`, unchanged `-s`
282+
status filtering, and a clear failure for the motivating malformed
283+
`--labels --all` ordering.
284+
2. Split filter names from label-field selection through
285+
`Commands.issue_list/1` and the `Issue` read action; update domain tests and
286+
`documents/ash-domain-erd.adoc` in the same change.
287+
3. Route the new flag through the existing display switch, update text/JSON
288+
tests, and preserve the existing output format.
289+
4. Update the README, generated help, and release notes.
290+
5. Run focused tests, `mix precommit`, and manual dogfooding through
291+
`mix lc` (never an alternate Linear client).
292+
293+
Implementation should use a feature branch from the then-current `main`; the
294+
plan branch contains documentation only. Conventional commit type: `feat`.
295+
296+
== Acceptance criteria
297+
298+
* `lcls -N --include-labels --all` and `lcls -N -i --all` list issues across
299+
assignees and lifecycle states and include their labels without adding a
300+
label filter.
301+
* `-i/--include-labels` never changes which issues match.
302+
* `-l/--labels` preserves case-insensitive, comma-separated OR filtering and
303+
makes matched issues' labels visible.
304+
* `-s/--status` retains its existing friendly-status filtering contract.
305+
* `lcls --labels --all` fails clearly before any API request rather than
306+
filtering for a label named `--all`.
307+
* `issue create -l/--labels VALUE` is unchanged.
308+
* Text and JSON output, paginated results, positional issue lookup, and the
309+
no-label fast path all have regression coverage.
310+
* Help, README, release guidance, tests, and the Ash domain ERD describe the
311+
same contract.

0 commit comments

Comments
 (0)