Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ and best practices for agents to follow.
- Phase 12 Plan: documents/phase-12-plan.adoc
- Phase 13 Plan: documents/phase-13-plan.adoc
- Phase 14 Plan: documents/phase-14-plan.adoc
- Phase 15 Plan: documents/phase-15-plan.adoc
- Phase 16 Plan: documents/phase-16-plan.adoc
- Burrito distribution decision: documents/burrito-decision.adoc
- Development and CI quality gates decision: documents/quality-gates-decision.adoc

## Project Structure

Expand Down
172 changes: 172 additions & 0 deletions documents/phase-15-plan.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
= {my-title}
Tj Vanderpoel (bougyman) <tj.vanderpoel@prizepicks.com>
:revdate: Sep 04, 2026
:my-title: Phase 15 plan: separate developer and CI quality gates
:icons: font
:env-github:
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:caution-caption: :fire:
:warning-caption: :warning:
endif::[]
:toc:

== Goal

Give the repository two purposeful validation entry points:

* `mix precommit` is a quick, self-contained command developers can run
repeatedly while editing; and
* `mix ci` is the complete, potentially slower integration gate used by GitHub
Actions and by developers who want CI-equivalent assurance.

The goal is not merely to rename commands. It is to prevent CI-only work from
silently accumulating in the everyday developer command.

== Context and supersession

Phase 14, decision 7, made `mix precommit` the comprehensive repository gate
and made `mix ci` a compatibility alias. The resulting implementation runs
dependency retrieval, security audits, repository-range validation, root and
app suites, and static analysis through `mix precommit`.

That is the wrong boundary for routine local use. This plan supersedes only
Phase 14 decision 7 and its related task/workflow wording. Phase 14's goals
for Conventional Commit and pull-request-title enforcement remain in force;
they move to the CI gate where they can use the PR event data and a complete
Git history. EXT-32 and EXT-33 must be reconciled with this plan before their
remaining work merges.

The enduring policy is recorded in
`documents/quality-gates-decision.adoc`.

== Gate contracts

=== `mix precommit`: frequent local feedback

`mix precommit` must be usable from an ordinary checkout after dependencies
are already installed. It must not download packages, fetch Git refs, require
credentials, start a database or other service, invoke a container runtime, or
build/package release artifacts.

It owns deterministic, source-local feedback:

. root formatting and root unit tests;
. app formatting, compilation/static analysis, and only the app tests that are
explicitly classified as fast unit tests; and
. any similarly local check added later.

The implementation target is a warm run in no more than five seconds on an ordinary
developer checkout. Measure it and record the result in the task documentation
or its test/issue evidence. A check that materially exceeds that budget belongs
in `mix ci` unless there is a deliberate revision of this plan.

=== `mix ci`: complete integration assurance

`mix ci` is the canonical pull-request and merge gate. It prepares the
environment, runs `mix precommit`, then runs the remaining CI-only work.
It may fetch dependencies and Git history, consume PR metadata, access
networked advisory services, provision databases or other test services, and
run integration, end-to-end, packaging, native-target, or release checks.

The required relationship is:

----
mix precommit ⊂ mix ci
----

CI-only checks must be listed in `mix help ci` and remain independently
identifiable in workflow output. A green `mix precommit` means the local gate
passed; it must not be presented as a prediction that all CI checks will pass.

== Implementation plan

=== 1. Classify the current checks and tests

Inventory every command currently run by `mix precommit`, `mix ci`, and the
GitHub workflows. For each, record its owner, dependencies, network/service
requirements, and warm-run cost.

Split the app suite deliberately rather than relying on a naming convention or
a vague assumption that all `mix test` tests are local. Add an explicit test
classification mechanism for slow, integration, service-backed, native, or
end-to-end tests. The default app test command used by `precommit` must select
only the fast unit category; `ci` must run both that category and the excluded
CI category. SQLite files created privately by tests are acceptable only when
they remain self-contained and within the local time budget; an externally
managed database is CI-only.

=== 2. Implement the local gate

Replace the comprehensive `Mix.Tasks.Precommit` sequence with the classified
local checks. It must reject arguments consistently, preserve injectable task
execution for unit tests, and document that dependencies must already exist.

Remove from it, at minimum, `deps.get`, Hex/dependency audits, pull-request
title validation, remote/range Git validation that can fetch, and every
CI-classified test. Add regression tests that assert both the included order
and the absence of CI-only commands.

=== 3. Implement the CI gate

Make `Mix.Tasks.Ci` an independent orchestrator, not an alias. It performs
environment/bootstrap work as needed, invokes the local gate, and then runs
the CI-only checks. It owns dependency and security audits, PR-title and
commit-range validation, and the classified integration/service suites.

Design the task so local CI-equivalent runs can state their prerequisites and
fail clearly if a required service is unavailable. Do not conceal external
setup inside a nominally local task. Add unit tests for sequencing, error
propagation, and the strict-superset relationship.

=== 4. Rewire workflows and guidance

Update `.github/workflows/ci.yaml` to run `mix ci`, supplying the Git history
and pull-request-title environment only to the CI gate. Retain the Phase 14
security rule: user-controlled titles are environment data, never shell source.
Ensure the root required workflow still exercises Conventional Commit and PR
title validation after it stops calling `mix precommit` directly.

Update the README, task module documentation, agent prompts, and Phase 14
references that currently call `mix precommit` the full gate. They must make
the distinction clear: run `mix precommit` frequently; run `mix ci` before a
PR when practical and rely on it as the required remote gate.

=== 5. Verify the contracts

Verify from a warm checkout that `mix precommit` neither uses the network nor
starts/depends on external services. Verify that a clean CI environment can
run `mix ci` end to end, including its bootstrap and service-backed checks.

Test failures at each boundary: a formatting/unit-test failure must fail both
commands; a CI-only audit, metadata, or integration failure must fail only
`mix ci`. Confirm workflow runs report the CI gate and preserve Phase 14's
required protection behavior.

== Linear issue decomposition

1. *Classify test and check tiers.* Define the explicit fast-versus-CI-only
test selection, inventory current costs/dependencies, and add enforcement
tests for the classification.
2. *Restore `mix precommit` as a local gate.* Implement the lightweight task,
documentation, and task tests after classification is available.
3. *Make `mix ci` the full integration gate.* Implement CI orchestration,
bootstrap/audits/metadata/integration checks, workflow rewiring, and
guidance updates. This follows the first two issues and reconciles EXT-32
and EXT-33 before their remaining changes land.

== Acceptance criteria

* `mix ci` is no longer an alias for `mix precommit`.
* A warm `mix precommit` completes in no more than five seconds on an ordinary
developer checkout.
* `mix precommit` needs no network, credentials, Git fetch, container runtime,
externally managed database, or other service after dependencies are present.
* `mix ci` runs every `mix precommit` check plus documented CI-only checks.
* Tests enforce both the command sequences and the excluded CI-only work.
* GitHub Actions calls `mix ci`; Conventional Commit and PR-title validation
remain required CI behavior.
* Developer documentation describes the two commands without calling the local
gate a complete CI prediction.
189 changes: 189 additions & 0 deletions documents/phase-16-plan.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
= {my-title}
Tj Vanderpoel (bougyman) <tj.vanderpoel@prizepicks.com>
:revdate: Sep 04, 2026
:my-title: Phase 16 plan: issue relationship support
:icons: font
:env-github:
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:caution-caption: :fire:
:warning-caption: :warning:
endif::[]
:toc:

== Goal

Make issue relationships first-class in `lc` so users can inspect and manage
dependencies without leaving the terminal. In particular, a planner must be
able to express that one issue is blocked by another and verify the resulting
Linear dependency link.

== Context

`mix lc issue create` and `mix lc issue update` currently have no relationship
operation. The documented ordering of related work therefore remains prose in
issue descriptions rather than a real Linear dependency graph.

Linear's checked-in GraphQL schema already provides the necessary API:

* `Issue.relations` and `Issue.inverseRelations` for outgoing and incoming
links;
* `issueRelationCreate`, `issueRelationDelete`, and `issueRelationUpdate`
mutations; and
* the `blocks`, `related`, and `duplicate` relation types.

The source endpoint of a `blocks` relation blocks the related endpoint. That
wire-level direction must not force users to mentally reverse a normal
dependency statement.

== Command design

Add an `issue relation` command group with three initial leaf commands:

[source,sh]
----
# Show both directions for EXT-38.
lc issue relation list EXT-38

# State the dependency in the natural direction: EXT-38 is blocked by EXT-37.
lc issue relation add EXT-38 EXT-37 --type blocked-by

# One issue may be blocked by any number of issues in one command.
lc issue relation add EXT-39 EXT-37 EXT-38 --type blocked-by

# Remove one or more relationships using the same natural statement.
lc issue relation remove EXT-39 EXT-37 EXT-38 --type blocked-by
----

`list` has the `ls` alias. The first positional argument is always the subject
of the statement and every following `RELATED_ISSUE` is an independent other
issue; command order is therefore never inferred from a relation ID or the
order returned by Linear. `add` and `remove` require one subject plus one or
more related issues.

[cols="1,3", options="header"]
|===
| Command
| Meaning

| `lc issue relation add A B... --type blocks`
| A blocks every named B.

| `lc issue relation add A B... --type blocked-by`
| A is blocked by every named B. The implementation reverses each endpoint
pair and sends a GraphQL `blocks` relation from each B to A.

| `lc issue relation add A B... --type related`
| A is related to every named B.

| `lc issue relation add A B... --type duplicate`
| A is a duplicate of every named B.
|===

`--type` is required and accepts exactly `blocks`, `blocked-by`, `related`,
and `duplicate`. `blocked-by` is a CLI convenience, not a GraphQL type.

`list` presents outbound and inbound relations separately, with the related
issue identifier and title, relation type in the user's direction, and the
relation ID. Human output must say `Blocked by` for an incoming `blocks`
relation and `Blocks` for an outgoing one. JSON output must retain the API
relation ID, source issue, related issue, wire type, and direction so scripts
do not need to parse terminal prose. The human output must have distinct
`Blocks` and `Blocked by` sections; each line names the other issue, so a user
can see the direction without reconstructing it from argument order.

`add` and `remove` are idempotent per target from the user's perspective:
adding an already-present identical relation succeeds with an informative
no-change result; removing an absent relation does the same. A multi-target
operation attempts every valid target and reports a result for each one; it
does not roll back successful independent mutations because Linear supplies no
transaction spanning relations. Its non-zero exit status and machine-readable
output must identify every failed target. Reject a self-link and ambiguous or
unknown relation type values before making that target's API request. Do not
add a generic relation-update command in this phase; a type change is an
explicit remove/add operation until its user-facing semantics are designed.

== Domain and API design

Add `LinearCli.Linear.IssueRelation` as an Ash resource registered in
`LinearCli.Linear`. It has manual read/create/destroy actions backed directly
by `LinearCli.Api`; no Ecto data layer and no Ash DSL relationship declaration
is introduced.

Expose named domain code interfaces for listing an issue's relations, creating
one, and deleting one. The CLI layer resolves human issue identifiers through
the existing issue lookup helpers, normalizes the directional `--type` value,
and calls those interfaces rather than constructing GraphQL documents itself.

The GraphQL selection must retrieve enough data to render and safely remove a
relation: relation ID and type plus both endpoint issues' IDs, identifiers,
titles, and URLs. Read both `relations` and `inverseRelations`, handle their
pagination, and preserve the API's error response rather than treating an
authorization or validation error as an empty list.

For deletion, first resolve the exact relation from the two supplied issues and
normalized type. If Linear returns duplicate rows for an impossible/legacy
state, fail with a diagnostic that names every matching relation ID instead of
deleting arbitrarily. Treat Linear's normal duplicate/invalid mutation errors
as actionable CLI errors. Do not attempt to infer or alter parent/sub-issue
hierarchies; they are distinct from issue relations.

== CLI integration and display

Extend the Optimus command specification, dispatch, aliases, and help text for
the `issue relation` group, including `lc issue relation ls ISSUE` as the
documented alias for `list`. Follow existing `issue view` lookup behavior for
issue identifiers, including expansion of a bare numeric ID where that helper
supports it. The help synopsis must name the arguments `ISSUE` and
`RELATED_ISSUE...` and include the directional table above.

Enhance full `issue view` output to show relationships through the same display
formatter used by `issue relation list`; compact issue lists remain unchanged.
The explicit `issue relation list` command is still required for discoverable,
scriptable relationship inspection.

Successful mutations print the created or removed relationship in human and
JSON output. Do not use a generic success sentence that hides the direction;
the output must make it obvious whether the named issue now blocks or is
blocked by the other one.

== Tests, documentation, and safety

Add unit tests at each boundary:

* resource/manual-action tests for GraphQL documents, variables, response
mapping, pagination, and API error propagation;
* domain code-interface tests;
* CLI/parser tests for every command, aliases, required `--type`, one-or-more
related issues, invalid types, self-links, reversed `blocked-by` variables,
per-target idempotency/partial failure, and ambiguous deletion; and
* display tests for both directions and JSON's stable relation fields.

Update `documents/ash-domain-erd.adoc` in the same change to add the resource,
its public attributes, code interfaces, nested endpoint associations, and the
explicit statement that these remain GraphQL/nested associations rather than
Ash DSL relationships. Update the README's development/CLI usage material and
any command reference or completion tests.

Use only `mix lc` for Linear interaction in repository documentation and agent
workflows. Verify against a disposable pair of test issues: create every
supported type, list from both endpoints, remove it, and confirm that repeated
add/remove operations have the documented idempotent result.

== Acceptance criteria

* `lc issue relation list` (and `ls`), `add`, and `remove` are discoverable in
`--help` and work with human and JSON output.
* `--type blocked-by` creates and removes the correctly reversed GraphQL
`blocks` relation.
* One `add` or `remove` command accepts an arbitrary non-empty list of related
issues and reports a separate outcome for every target.
* Both endpoints show the same relationship in their natural direction.
* Duplicate adds and absent removes are safe no-ops; self-links and ambiguity
fail before unintended mutation.
* The domain resource, code interfaces, CLI tests, display tests, README, and
Ash domain ERD remain in sync.
* No raw GraphQL or alternate Linear client is introduced outside
`LinearCli.Api`.
Loading