Skip to content

feat: honor repeated --label and paginate past 50 results - #38

Closed
seraph-pixelperfect wants to merge 1 commit into
feat/issues-searchfrom
feat/multi-label-pagination
Closed

seraph-pixelperfect wants to merge 1 commit into
feat/issues-searchfrom
feat/multi-label-pagination

Conversation

@seraph-pixelperfect

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #37 (feat/issues-search) — review that one first. This branch adds a single commit (230c540) on top of e1ef299.

Closes #18. Depends on #37.

Two behaviors land together, per the issue:

  1. Multi-label filtering — every repeated --label is now honored via labels: { some: { name: { in: [...] } } } ("any of" semantics: an issue matches when it carries at least one of the given labels). The old single-label filter.label (labels: { name: { eq: ... } }) and the "Only the first label was applied — multi-label filter lands post-v1" hint are removed. The filter composes into the searchIssues query too, so --search + multiple labels work together.
  2. Auto-pagination — fetchIssues now requests pageInfo { hasNextPage endCursor } and follows the cursor in 50-size batches until --limit is satisfied or the server reports no more pages. Each page asks for only the remaining slots (min(limit - collected, 50)), so we never over-fetch. --limit cap raised from 50 to 500.

Verified comparator shape (schema source)

labels: { some: { name: { in: [...] } } } matches the live schema exactly — no deviation from the issue's stated shape was needed. Verified against the generated GraphQL type documents in @linear/sdk@90.0.0 (npm pack @linear/sdk, then dist/index-zxW4m1xd.d.mts):

  • IssueFilter.labels?: IssueLabelCollectionFilter — "Filters that issue labels must satisfy."
  • IssueLabelCollectionFilter.some?: IssueLabelFilter — "Filters that needs to be matched by some issue labels." (every also exists; some is the correct "any of" combinator, so the issue's shape is schema-valid as written.)
  • IssueLabelFilter.name?: StringComparator
  • StringComparator.in?: Array<String> — "In-array constraint."

Pagination support was verified the same way: SearchIssuesQueryVariables includes after?: String alongside first?: Int, and both IssueConnection and IssueSearchPayload expose pageInfo: PageInfo (hasNextPage: Boolean!, endCursor?: String). So --search results paginate through the same loop as plain issues.

Demo (real commands, this branch)

Help text documents the new semantics and cap:

$ pnpm exec tsx bin/linear-axi.ts issues --help
usage: linear-axi issues [--team <KEY>] [--state <type>] [--assignee <name|me>] [--label <name>] [--search <text>] [--limit <n>]
	List Linear issues, most recently updated first (relevance-ranked when --search is given).

flags:
  --team <KEY>       filter by team key (e.g. LIN)
  --state <type>     workflow state type: backlog, unstarted, started, completed, canceled, triage
  --assignee <name>  "me" for yourself, or a user name
  --label <name>     filter by label name; repeat to match ANY of the given labels (issues carrying at least one)
  --search <text>    full-text search (same ranking as Linear's app search); composable with the filters above
  --limit <n>        max issues to return (default 25, max 500; fetched in pages of 50)

examples:
  ...
  linear-axi issues --label bug --label regression

Before/after — the exact query this branch sends for --label a --label b --limit 200 (debug dump via LINEAR_AXI_DEBUG=1, reaching the real API layer; 401 is expected with a fake key):

$ LINEAR_AXI_DEBUG=1 pnpm exec tsx bin/linear-axi.ts issues --label bug --label regression --key lin_api_fake --limit 200
[linear-axi] query: query Issues($first: Int!, $after: String) {
        issues(filter: { labels: { some: { name: { in: ["bug","regression"] } } } }, first: $first, after: $after, orderBy: updatedAt) {
          nodes { ... }
          pageInfo { hasNextPage endCursor }
        }
      }
[linear-axi] variables: {"first":50}
[linear-axi] raw body: {"errors":[{"message":"Authentication required, not authenticated",...,"statusCode":401}]}
error: Linear auth required — set LINEAR_API_KEY or pass --key <key>

Before (base e1ef299): the same command sent filter: { labels: { name: { eq: "bug" } } } — first label only, no after, no pageInfo, hard first: min(limit, 50) — and printed a hint that multi-label lands post-v1.

Limit enforcement is loud, not silent:

$ pnpm exec tsx bin/linear-axi.ts issues --limit 501 --key lin_api_fake
error: "Invalid --limit: 501 (max 500)"
code: VALIDATION_ERROR
help[1]: Results are fetched in pages of 50; the total is capped for token safety.

Local gates (all run on this branch)

$ pnpm build          → tsc, clean (no output)
$ pnpm lint           → eslint ., clean (no output)
$ pnpm run build:skill -- --check
skills/linear-axi/SKILL.md is up to date.

$ pnpm test
 Test Files  8 passed (8)
      Tests  63 passed (63)

$ pnpm run format:check
[warn] Code style issues found in 18 files. Run Prettier with --write to fix.

63 = 49 pre-existing (all still passing) + 12 new in test/issues-pagination.test.ts + 2 new hasMore cases in test/format.test.ts. No test was deleted. format:check is the known pre-existing red (same 18 files as on the base branch; prettier 3.9.6 drift, not run in CI). New/edited files that were clean before (src/format.ts, src/skill.ts, test/format.test.ts, test/issues-search.test.ts) remain clean, and the new test/issues-pagination.test.ts passes prettier --check.

One existing test was updated in place, not weakened: test/issues-search.test.ts ("composes search with team + state + label") used the removed IssueListFilter.label key and asserted the old labels: { name: { eq: "bug" } } shape; it now uses labels: ['bug'] and asserts labels: { some: { name: { in: ["bug"] } } }. This mirrors the deliberate behavior change in this PR.

Design notes & edge cases flagged

  • Clamp vs reject for --limit > 500: chose loud rejection (exit 2, VALIDATION_ERROR) over silent clamping, per AXI principle 6 (fail loud) — consistent with how the CLI treats unknown flags and blank flag values. Help text says "max 500" rather than "capped at" to match.
  • Short page + hasNextPage: true: if the API returns fewer nodes than requested while saying more exist, we keep paging (a short page is not exhaustion — Linear may legitimately return partial pages). Covered by a test.
  • Defensive loop guards: if a page returns 0 nodes, or endCursor is missing/non-advancing while hasNextPage stays true, we stop rather than loop forever; hasMore retains the server's claim, so the truncation hint still shows.
  • Truncation hint accuracy: fetchIssues now returns hasMore (last page's hasNextPage) and formatCountLine accepts it. When hasMore: false and count === limit, the "(showing first N)" hint is suppressed — exactly-N matches is not truncation. hasMore: undefined falls back to the old count === limit heuristic, so other callers are unaffected.
  • totalCount not used: IssueSearchPayload does expose totalCount, but the plain issues connection does not, and mixing count-line styles between --search and normal listing would be inconsistent — kept the slice-based count + hasMore hint everywhere.
  • skills/linear-axi/SKILL.md was regenerated (pnpm run build:skill) after updating one tip line in src/skill.ts to state the any-of semantics; the --check gate passes.

Stacked-PR / CI notes

  • This PR is stacked on feat: add issues --search full-text search #37 (feat/issues-search) and targets feat/issues-search, not main. GitHub will show no CI checks for it because ci.yml only triggers on PRs targeting main — CI will run when this PR is retargeted to main after feat: add issues --search full-text search #37 merges; the local gates pasted above are the evidence. It should not be retargeted as part of this review.
  • CI quota-block caveat: some Actions jobs in this org get rejected pre-execution with a billing/spending-limit message, which gh pr checks renders as a failure. If a check looks red, inspect gh run view <run-id> / the jobs API for an empty steps array and a ~3-4s duration before assuming breakage; if quota-blocked, treat the local output above as authoritative. Jobs are not retried.
  • Opened by an automated agent dispatch authenticated as seraph-pixelperfect, for human review — not self-approved. Merge is a human decision.

Honor every repeated --label via labels: { some: { name: { in: [...] } } }
(any-of semantics, documented in --help) and auto-paginate fetchIssues in
50-size batches using pageInfo { hasNextPage endCursor } until --limit is
satisfied. Raise the --limit cap to 500 (rejected loudly above it), keep the
truncation hint accurate via pageInfo.hasNextPage, and drop the old
first-label-only filter + hint.

Closes #18
@ghostinprod-pixelperfect

Copy link
Copy Markdown
Collaborator

Superseded by merged rc/0.1 PR #51 (commit 694a564), which consolidated this read/discovery stack with validation and review fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants