github source v1

This commit is contained in:
stoorps 2026-03-19 20:14:39 +00:00
parent 71f89dde9c
commit caf870d05e
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
50 changed files with 4139 additions and 131 deletions

View file

@ -0,0 +1,333 @@
# GitHub Source End-to-End Design
## Goal
Extend `aim-core` so GitHub-backed installs and updates work end to end across repo shorthand, GitHub URLs, and direct GitHub release asset URLs, while keeping the architecture reusable for future sources and a later GUI client.
The design should support broad discovery, metadata-aware artifact selection, durable update-channel fallback, and a clean separation between source discovery, metadata parsing, and update decision-making.
## Agreed Product Shape
### Supported GitHub entry forms
- `owner/repo`
- GitHub repo URL
- GitHub release URL
- direct GitHub release asset URL
### Canonical identity and source behavior
- Canonical app identity should resolve to the best available app identity, usually the GitHub repository when that can be determined confidently
- The system should preserve the original user input and normalized source references for auditability and later recovery
- If the user points at an older release or a prerelease directly, the system should respect that context rather than silently switching to an unrelated channel
### Discovery and artifact selection behavior
- Discovery should be broad enough to inspect GitHub releases, candidate AppImage assets, and adjacent metadata files
- Metadata should influence both install-time artifact selection and future update behavior
- Artifact selection should prefer metadata-guided choices first and filename heuristics second
- Stable releases should be preferred by default, unless the user explicitly started from a prerelease
### Update-channel behavior
- The preferred update channel should, by default, match the install origin as closely as possible
- The registry should retain as many validated alternate channels as practical so future updates can fall back automatically if the preferred path fails
- `aim` should be able to explain why a particular channel or artifact was chosen
## Recommended Architecture
Use a three-part core model inside `aim-core`:
- `source` discovers and fetches candidate release information, assets, and metadata documents
- `metadata` parses source-agnostic metadata formats such as `electron-builder` YAML or zsync-related metadata
- `update` ranks channels and artifacts, applies product rules, and persists a durable update strategy
This architecture was selected over keeping all upstream logic under `adapters`, because formats like `latest-linux.yml` and zsync metadata are not fundamentally tied to a single provider. They are metadata formats that can be discovered from different source types and should remain reusable.
## Core Boundary Decision
The critical rule is:
- `source` discovers
- `metadata` interprets
- `update` decides
- `app` orchestrates
That boundary keeps source-specific fetching logic separate from metadata parsing and prevents update rules from leaking into transport or parser code.
## Proposed Module Layout
Within `crates/aim-core/src/`, the design should evolve toward:
- `app/`
- `domain/`
- `source/`
- `metadata/`
- `update/`
- `registry/`
- `integration/`
- `platform/`
Responsibilities:
### `app`
- orchestration layer for add, update, list, and remove flows
- interactive decision boundaries
- conversion of lower-level results into user-facing plans and prompts
### `source`
- normalize user input into typed source references
- resolve GitHub shorthand, repo URLs, release URLs, and asset URLs
- fetch releases, assets, and candidate metadata documents
- detect candidate update channels without interpreting metadata semantics deeply
### `metadata`
- source-agnostic parsers for metadata document formats
- initial parsers should cover:
- `electron-builder` Linux metadata such as `latest-linux.yml`
- zsync-related metadata or update hints
- return structured hints, warnings, and confidence rather than UI-facing decisions
### `update`
- combine source discovery and parsed metadata
- rank channels and candidate artifacts
- apply install-origin-first default prioritization
- retain alternate channels for fallback
- build persisted update strategies and reviewable plans
### `domain` and `registry`
- `domain` holds the stable source-agnostic types used across the core
- `registry` stores only the durable information needed to explain and recover update behavior later
## Discovery And Update Flow
### 1. Input resolution
`source::resolve_input` should accept:
- `owner/repo`
- GitHub repo URL
- GitHub release URL
- direct release asset URL
- generic URL fallback
It should classify the input, retain the raw source input, and derive a normalized source reference. When possible, it should also derive a canonical app identity anchored to the GitHub repository.
### 2. Broad source discovery
The source layer should:
- fetch repository and release context
- enumerate candidate AppImage assets
- detect adjacent metadata files such as `latest-linux.yml`
- detect related update artifacts such as zsync files or embedded update hints when available
If the user supplied a link to an older release, discovery should still be broad enough to see newer releases, while preserving enough context for the application layer to ask whether the user wants to track that release lineage or switch to latest-supported updates.
### 3. Metadata parsing
Each metadata parser should consume raw document bytes plus lightweight fetch context and return structured hints such as:
- version
- artifact URL
- checksum or digest information
- channel or release label
- architecture or platform compatibility
- updater-family identity
- parser confidence
- warnings
Metadata parsing must remain source-agnostic. A GitHub discovery flow may hand documents to the parser, but the parser should not depend on GitHub-specific assumptions.
### 4. Channel construction
The update layer should turn source and metadata results into reusable channel records, for example:
- GitHub releases API channel
- `electron-builder` metadata channel
- zsync-derived channel
- direct-asset-lineage channel
Each channel should record:
- how it was discovered
- what artifacts it can produce
- its confidence and compatibility scope
- whether it matches the original install origin
### 5. Ranking and persistence
The update layer should select:
- one preferred channel
- an ordered list of validated alternate channels
Default rule:
- prefer the channel that best matches how the app was originally installed
- retain all other validated channels in fallback order
Artifact selection rule:
- metadata-guided first
- filename heuristics second
Prerelease rule:
- only prefer prerelease channels when the user explicitly started from one
- otherwise prefer stable releases first
## Domain Model
The design should introduce or evolve the following stable concepts.
### `AppIdentity`
- canonical tracked-app identity
- usually GitHub repository identity when resolvable
- includes normalized key and display name data
### `SourceInput`
- the exact user-provided input
- shorthand, repo URL, release URL, asset URL, or generic URL
### `SourceRef`
- normalized source locator used for discovery
- may represent a GitHub repo, a release lineage, a direct asset lineage, or another supported source pattern
### `MetadataDocument`
- raw fetched metadata plus provenance
- includes source URL, content type, fetch time, digest, document type guess, and raw content or a blob reference
### `MetadataHint`
- parsed interpretation of a metadata document
- includes install and update hints such as version, artifact URL, checksum, architecture, warnings, and confidence
### `UpdateChannel`
- a durable description of one update path
- examples include `github-releases`, `electron-builder`, `zsync`, and `direct-asset-lineage`
### `ArtifactCandidate`
- one installable AppImage candidate
- includes URL or path, version, architecture, provenance, checksum data, and score explanation
### `UpdateStrategy`
- preferred channel
- alternate channels in order
- preference reason such as install-origin match or stronger metadata confidence
## Registry Model
The registry should persist only the information that matters for future updates, explainability, and recovery.
Persist:
- canonical `AppIdentity`
- original `SourceInput`
- normalized `SourceRef`
- installed version and artifact state when known
- `UpdateStrategy`
- retained `UpdateChannel` records
- useful parsed metadata hints
- selected raw metadata snapshots or references
Do not persist every transient discovery result. The registry is not a general fetch cache. It should keep enough information to:
- explain why a channel was chosen
- retry alternates later
- survive upstream layout changes
- support debugging of bad matches
## Failure Handling
### Source failures
- source failures should be local, not globally fatal by default
- if GitHub API discovery fails but a direct asset URL remains usable, installation should continue through that path
- if one candidate discovery branch fails, the system should keep evaluating other validated branches where possible
### Metadata failures
- metadata parse failures should be typed and non-blocking
- failure to parse `latest-linux.yml` must not disable plain GitHub release discovery
- low-confidence metadata may still contribute hints, but should not outrank stronger direct evidence
### Update selection explanations
The update layer should be able to explain why a channel or artifact was preferred or rejected, using signals such as:
- install-origin match
- metadata confidence
- architecture compatibility
- prerelease mismatch
- stale, missing, or incompatible artifacts
## User Prompting Rules
Prompt only when the ambiguity materially changes behavior.
Expected prompt cases:
- the user linked an older specific release and the system can also see newer supported releases
- multiple AppImage artifacts remain equally plausible after metadata and heuristic ranking
- identity resolution remains low confidence after source normalization
Everything else should be automatic and explainable in the generated plan.
## Testing Strategy
### Source tests
- normalize shorthand, repo URLs, release URLs, and asset URLs
- verify broad discovery from mocked GitHub responses
- preserve prompt context for older-version inputs
### Metadata tests
- fixture-driven parsing for `electron-builder` YAML, zsync, and malformed inputs
- confidence and warning behavior
- source-agnostic parser contract
### Update tests
- install-origin-first ranking
- alternate-channel fallback ordering
- stable-versus-prerelease preference
- metadata-guided artifact selection outranking filename heuristics
### Registry tests
- round-trip persistence of preferred channel, alternates, metadata hints, and metadata snapshots
- backward-compatible loading when existing registry entries lack new fields
### End-to-end CLI tests
- add from GitHub shorthand
- add from direct GitHub release asset URL
- update behavior when preferred channel fails and an alternate succeeds
## Recommended Implementation Direction
Implementation should incrementally reshape the current adapter-oriented GitHub skeleton toward the new boundaries rather than attempt a single large rewrite.
Recommended sequence:
1. introduce `source`, `metadata`, and `update` domain boundaries and shared types
2. migrate GitHub discovery logic into the new source boundary
3. add metadata parsers and fixture coverage
4. add channel ranking and registry persistence extensions
5. update CLI orchestration and end-to-end tests
This keeps the change reviewable and protects the existing thin-client architecture where `aim-core` owns all reusable logic.

View file

@ -0,0 +1,644 @@
# GitHub Source End-to-End Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add end-to-end GitHub source support in `aim-core` for shorthand, repo URLs, release URLs, and direct asset URLs, with source-agnostic metadata parsing, install-origin-first channel selection, and registry-backed fallback channels.
**Architecture:** Reshape the current GitHub skeleton from an adapter-centric model into explicit `source`, `metadata`, and `update` boundaries. Keep `aim-cli` thin by moving normalization, metadata interpretation, channel ranking, and recovery behavior into `aim-core`, then extend the registry so future updates can survive upstream changes.
**Tech Stack:** Rust, Cargo workspace, serde, toml, reqwest-compatible fetch abstractions, clap, dialoguer, assert_cmd, predicates, fixture-driven tests in `crates/aim-core/tests` and `crates/aim-cli/tests`.
---
### Task 1: Introduce the new core boundary modules and types
**Files:**
- Create: `crates/aim-core/src/source/mod.rs`
- Create: `crates/aim-core/src/source/input.rs`
- Create: `crates/aim-core/src/source/github.rs`
- Create: `crates/aim-core/src/metadata/mod.rs`
- Create: `crates/aim-core/src/metadata/document.rs`
- Create: `crates/aim-core/src/update/mod.rs`
- Modify: `crates/aim-core/src/lib.rs`
- Modify: `crates/aim-core/src/domain/source.rs`
- Modify: `crates/aim-core/src/domain/update.rs`
- Test: `crates/aim-core/tests/query_resolution.rs`
**Step 1: Write the failing test**
```rust
use aim_core::source::input::{classify_input, SourceInputKind};
#[test]
fn classifies_github_release_asset_url() {
let input = classify_input(
"https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage",
)
.unwrap();
assert_eq!(input.kind, SourceInputKind::GitHubReleaseAssetUrl);
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test classifies_github_release_asset_url --package aim-core --test query_resolution`
Expected: FAIL because the `source` module and new input classification types do not exist yet
**Step 3: Write minimal implementation**
Add the new top-level modules and export them from `aim_core`. Introduce the minimum source and update domain types needed to classify GitHub inputs without rewriting existing workflows yet.
**Step 4: Run test to verify it passes**
Run: `cargo test classifies_github_release_asset_url --package aim-core --test query_resolution`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/lib.rs crates/aim-core/src/source crates/aim-core/src/metadata crates/aim-core/src/update crates/aim-core/src/domain/source.rs crates/aim-core/src/domain/update.rs crates/aim-core/tests/query_resolution.rs
git commit -m "feat: add source metadata and update module boundaries"
```
### Task 2: Implement GitHub input normalization across all supported entry forms
**Files:**
- Modify: `crates/aim-core/src/source/input.rs`
- Modify: `crates/aim-core/src/source/github.rs`
- Modify: `crates/aim-core/src/app/query.rs`
- Modify: `crates/aim-core/src/app/identity.rs`
- Test: `crates/aim-core/tests/query_resolution.rs`
- Test: `crates/aim-core/tests/identity_resolution.rs`
**Step 1: Write the failing test**
```rust
use aim_core::app::query::resolve_query;
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind};
#[test]
fn resolves_owner_repo_to_github_repo_source() {
let source = resolve_query("sharkdp/bat").unwrap();
assert_eq!(source.input_kind, SourceInputKind::RepoShorthand);
assert_eq!(source.normalized_kind, NormalizedSourceKind::GitHubRepository);
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test resolves_owner_repo_to_github_repo_source --package aim-core --test query_resolution`
Expected: FAIL because normalized GitHub source kinds are not represented yet
**Step 3: Write minimal implementation**
Teach query resolution and identity normalization to recognize:
- `owner/repo`
- GitHub repo URLs
- GitHub release URLs
- direct GitHub release asset URLs
Preserve the original input while returning a normalized source reference that can later drive discovery.
**Step 4: Run test to verify it passes**
Run: `cargo test resolves_owner_repo_to_github_repo_source --package aim-core --test query_resolution`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/source/input.rs crates/aim-core/src/source/github.rs crates/aim-core/src/app/query.rs crates/aim-core/src/app/identity.rs crates/aim-core/tests/query_resolution.rs crates/aim-core/tests/identity_resolution.rs
git commit -m "feat: normalize github input forms"
```
### Task 3: Add GitHub discovery records for releases, assets, and linked metadata
**Files:**
- Modify: `crates/aim-core/src/source/github.rs`
- Modify: `crates/aim-core/src/domain/source.rs`
- Create: `crates/aim-core/tests/github_source_discovery.rs`
- Modify: `crates/aim-core/src/adapters/test_support.rs`
**Step 1: Write the failing test**
```rust
use aim_core::source::github::discover_github_candidates;
#[test]
fn discovery_reports_appimage_assets_and_latest_linux_yml() {
let discovery = discover_github_candidates(/* mocked github response */).unwrap();
assert!(discovery
.assets
.iter()
.any(|asset| asset.name.ends_with(".AppImage")));
assert!(discovery
.metadata_documents
.iter()
.any(|doc| doc.url.ends_with("latest-linux.yml")));
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test discovery_reports_appimage_assets_and_latest_linux_yml --package aim-core --test github_source_discovery`
Expected: FAIL because source discovery does not yet return structured assets and metadata document records
**Step 3: Write minimal implementation**
Add GitHub discovery result types that expose:
- releases
- AppImage assets
- discovered metadata document URLs
- enough provenance to support later prompt and ranking logic
Use existing test-support scaffolding rather than real network calls.
**Step 4: Run test to verify it passes**
Run: `cargo test discovery_reports_appimage_assets_and_latest_linux_yml --package aim-core --test github_source_discovery`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/source/github.rs crates/aim-core/src/domain/source.rs crates/aim-core/src/adapters/test_support.rs crates/aim-core/tests/github_source_discovery.rs
git commit -m "feat: add github source discovery records"
```
### Task 4: Add source-agnostic metadata document and parser contracts
**Files:**
- Modify: `crates/aim-core/src/metadata/mod.rs`
- Modify: `crates/aim-core/src/metadata/document.rs`
- Create: `crates/aim-core/src/metadata/parser.rs`
- Modify: `crates/aim-core/src/domain/update.rs`
- Create: `crates/aim-core/tests/metadata_contract.rs`
**Step 1: Write the failing test**
```rust
use aim_core::metadata::{parse_document, MetadataDocument, ParsedMetadataKind};
#[test]
fn unknown_document_returns_typed_warning_not_panic() {
let doc = MetadataDocument::plain_text("https://example.test/notes.txt", b"not metadata");
let result = parse_document(&doc).unwrap();
assert_eq!(result.kind, ParsedMetadataKind::Unknown);
assert!(!result.warnings.is_empty());
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test unknown_document_returns_typed_warning_not_panic --package aim-core --test metadata_contract`
Expected: FAIL because the metadata parsing contract does not exist yet
**Step 3: Write minimal implementation**
Define:
- metadata document input type
- metadata parse result type
- source-agnostic parser entry point
- typed warnings for unsupported or malformed documents
Keep the implementation minimal and independent from GitHub-specific code.
**Step 4: Run test to verify it passes**
Run: `cargo test unknown_document_returns_typed_warning_not_panic --package aim-core --test metadata_contract`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/metadata crates/aim-core/src/domain/update.rs crates/aim-core/tests/metadata_contract.rs
git commit -m "feat: add metadata parser contract"
```
### Task 5: Implement `electron-builder` Linux metadata parsing
**Files:**
- Create: `crates/aim-core/src/metadata/electron_builder.rs`
- Modify: `crates/aim-core/src/metadata/mod.rs`
- Test: `crates/aim-core/tests/metadata_electron_builder.rs`
- Create: `crates/aim-core/tests/fixtures/latest-linux.yml`
**Step 1: Write the failing test**
```rust
use aim_core::metadata::{parse_document, MetadataDocument, ParsedMetadataKind};
#[test]
fn parses_latest_linux_yml_into_download_hints() {
let raw = include_bytes!("fixtures/latest-linux.yml");
let doc = MetadataDocument::yaml("https://example.test/latest-linux.yml", raw);
let result = parse_document(&doc).unwrap();
assert_eq!(result.kind, ParsedMetadataKind::ElectronBuilder);
assert_eq!(result.hints.primary_download.as_deref(), Some("T3-Code-0.0.11-x86_64.AppImage"));
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test parses_latest_linux_yml_into_download_hints --package aim-core --test metadata_electron_builder`
Expected: FAIL because `electron-builder` metadata is not parsed yet
**Step 3: Write minimal implementation**
Add an `electron_builder` parser that extracts:
- version
- primary download artifact
- checksum or digest when present
- architecture hints when available
- parser confidence and warnings
**Step 4: Run test to verify it passes**
Run: `cargo test parses_latest_linux_yml_into_download_hints --package aim-core --test metadata_electron_builder`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/metadata/electron_builder.rs crates/aim-core/src/metadata/mod.rs crates/aim-core/tests/metadata_electron_builder.rs crates/aim-core/tests/fixtures/latest-linux.yml
git commit -m "feat: parse electron builder linux metadata"
```
### Task 6: Implement zsync metadata parsing and channel hints
**Files:**
- Create: `crates/aim-core/src/metadata/zsync.rs`
- Modify: `crates/aim-core/src/metadata/mod.rs`
- Test: `crates/aim-core/tests/metadata_zsync.rs`
- Create: `crates/aim-core/tests/fixtures/example.zsync`
**Step 1: Write the failing test**
```rust
use aim_core::metadata::{parse_document, MetadataDocument, ParsedMetadataKind};
#[test]
fn parses_zsync_document_into_channel_hints() {
let raw = include_bytes!("fixtures/example.zsync");
let doc = MetadataDocument::plain_text("https://example.test/app.AppImage.zsync", raw);
let result = parse_document(&doc).unwrap();
assert_eq!(result.kind, ParsedMetadataKind::Zsync);
assert!(result.hints.primary_download.is_some());
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test parses_zsync_document_into_channel_hints --package aim-core --test metadata_zsync`
Expected: FAIL because zsync parsing does not exist yet
**Step 3: Write minimal implementation**
Add a zsync parser that extracts download URL, filename, version-like hints where possible, and channel confidence without coupling it to one upstream source.
**Step 4: Run test to verify it passes**
Run: `cargo test parses_zsync_document_into_channel_hints --package aim-core --test metadata_zsync`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/metadata/zsync.rs crates/aim-core/src/metadata/mod.rs crates/aim-core/tests/metadata_zsync.rs crates/aim-core/tests/fixtures/example.zsync
git commit -m "feat: parse zsync metadata documents"
```
### Task 7: Add update-channel ranking and artifact scoring
**Files:**
- Modify: `crates/aim-core/src/update/mod.rs`
- Create: `crates/aim-core/src/update/channels.rs`
- Create: `crates/aim-core/src/update/ranking.rs`
- Modify: `crates/aim-core/src/app/update.rs`
- Modify: `crates/aim-core/src/domain/update.rs`
- Modify: `crates/aim-core/tests/update_planning.rs`
**Step 1: Write the failing test**
```rust
use aim_core::update::ranking::rank_channels;
#[test]
fn install_origin_match_beats_higher_level_fallback() {
let ranked = rank_channels(/* preferred direct asset lineage */, /* github releases */, /* electron-builder */);
assert_eq!(ranked[0].reason.as_str(), "install-origin-match");
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test install_origin_match_beats_higher_level_fallback --package aim-core --test update_planning`
Expected: FAIL because channels and ranking reasons are not modeled yet
**Step 3: Write minimal implementation**
Implement channel and artifact ranking rules for:
- install-origin-first preference
- stable-over-prerelease by default
- metadata-guided artifact selection ahead of filename heuristics
- ordered alternates retained for fallback
**Step 4: Run test to verify it passes**
Run: `cargo test install_origin_match_beats_higher_level_fallback --package aim-core --test update_planning`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/update crates/aim-core/src/app/update.rs crates/aim-core/src/domain/update.rs crates/aim-core/tests/update_planning.rs
git commit -m "feat: add update channel ranking"
```
### Task 8: Extend the registry model for source input, strategy, and fallback channels
**Files:**
- Modify: `crates/aim-core/src/registry/model.rs`
- Modify: `crates/aim-core/src/registry/store.rs`
- Modify: `crates/aim-core/src/domain/app.rs`
- Modify: `crates/aim-core/tests/registry_roundtrip.rs`
**Step 1: Write the failing test**
```rust
use aim_core::registry::store::RegistryStore;
#[test]
fn registry_round_trips_update_strategy_and_alternates() {
let store = RegistryStore::new(/* temp path */);
let original = sample_record_with_strategy();
store.save(&[original.clone()]).unwrap();
let loaded = store.load().unwrap();
assert_eq!(loaded[0].update_strategy.preferred.reason, "install-origin-match");
assert_eq!(loaded[0].update_strategy.alternates.len(), 2);
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test registry_round_trips_update_strategy_and_alternates --package aim-core --test registry_roundtrip`
Expected: FAIL because the registry cannot persist the new strategy fields yet
**Step 3: Write minimal implementation**
Extend the registry model to persist:
- original source input
- normalized source reference
- preferred channel
- ordered alternates
- selected metadata hints or snapshot references
Keep loading backward-compatible for existing records that lack these fields.
**Step 4: Run test to verify it passes**
Run: `cargo test registry_round_trips_update_strategy_and_alternates --package aim-core --test registry_roundtrip`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/registry/model.rs crates/aim-core/src/registry/store.rs crates/aim-core/src/domain/app.rs crates/aim-core/tests/registry_roundtrip.rs
git commit -m "feat: persist update strategy and fallback channels"
```
### Task 9: Wire the add flow through source discovery, metadata parsing, and channel selection
**Files:**
- Modify: `crates/aim-core/src/app/add.rs`
- Modify: `crates/aim-core/src/app/identity.rs`
- Modify: `crates/aim-core/src/source/github.rs`
- Modify: `crates/aim-core/tests/github_add_flow.rs`
**Step 1: Write the failing test**
```rust
use aim_core::app::add::build_add_plan;
#[test]
fn add_plan_prefers_metadata_guided_appimage_when_available() {
let plan = build_add_plan(/* github shorthand with latest-linux.yml */).unwrap();
assert_eq!(plan.selected_artifact.selection_reason, "metadata-guided");
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test add_plan_prefers_metadata_guided_appimage_when_available --package aim-core --test github_add_flow`
Expected: FAIL because add planning does not yet route through metadata-aware ranking
**Step 3: Write minimal implementation**
Update add planning to:
- resolve normalized GitHub source input
- perform discovery
- parse any fetched metadata documents
- rank channels and artifacts
- emit a plan that records why the winning artifact was chosen
**Step 4: Run test to verify it passes**
Run: `cargo test add_plan_prefers_metadata_guided_appimage_when_available --package aim-core --test github_add_flow`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/app/add.rs crates/aim-core/src/app/identity.rs crates/aim-core/src/source/github.rs crates/aim-core/tests/github_add_flow.rs
git commit -m "feat: wire add flow through source and metadata pipeline"
```
### Task 10: Add prompt context for older releases and ambiguous artifacts
**Files:**
- Modify: `crates/aim-core/src/app/interaction.rs`
- Modify: `crates/aim-core/src/app/add.rs`
- Modify: `crates/aim-cli/src/ui/prompt.rs`
- Modify: `crates/aim-cli/tests/ui_summary.rs`
- Modify: `crates/aim-cli/tests/end_to_end_cli.rs`
**Step 1: Write the failing test**
```rust
use aim_core::app::add::build_add_plan;
#[test]
fn direct_old_release_url_requests_tracking_choice_prompt() {
let plan = build_add_plan(/* direct old github asset url with newer releases available */).unwrap();
assert!(plan
.interactions
.iter()
.any(|item| item.key == "tracking-preference"));
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test direct_old_release_url_requests_tracking_choice_prompt --package aim-core --test github_add_flow`
Expected: FAIL because the flow does not surface the new prompt context yet
**Step 3: Write minimal implementation**
Add typed prompt requests for:
- older explicit release versus latest-supported tracking
- ambiguous artifact ties after metadata and heuristics
Keep prompt rendering in `aim-cli`, but define the request shape in `aim-core`.
**Step 4: Run test to verify it passes**
Run: `cargo test direct_old_release_url_requests_tracking_choice_prompt --package aim-core --test github_add_flow`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/app/interaction.rs crates/aim-core/src/app/add.rs crates/aim-cli/src/ui/prompt.rs crates/aim-cli/tests/ui_summary.rs crates/aim-cli/tests/end_to_end_cli.rs
git commit -m "feat: add prompt support for github tracking choices"
```
### Task 11: Teach update planning to fall back when the preferred channel fails
**Files:**
- Modify: `crates/aim-core/src/app/update.rs`
- Modify: `crates/aim-core/src/update/ranking.rs`
- Modify: `crates/aim-core/tests/update_planning.rs`
- Modify: `crates/aim-cli/tests/end_to_end_cli.rs`
**Step 1: Write the failing test**
```rust
use aim_core::app::update::build_update_plan;
#[test]
fn update_plan_uses_alternate_channel_after_preferred_failure() {
let plan = build_update_plan(/* registry entry with failing preferred channel */).unwrap();
assert_eq!(plan.updates[0].selected_channel.kind.as_str(), "electron-builder");
assert_eq!(plan.updates[0].selection_reason.as_str(), "preferred-channel-failed");
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test update_plan_uses_alternate_channel_after_preferred_failure --package aim-core --test update_planning`
Expected: FAIL because update planning does not yet retry alternates
**Step 3: Write minimal implementation**
Teach update planning to:
- evaluate the preferred channel first
- fall back through ordered alternates when the preferred path is stale, broken, or incompatible
- preserve an explanation for the fallback decision
**Step 4: Run test to verify it passes**
Run: `cargo test update_plan_uses_alternate_channel_after_preferred_failure --package aim-core --test update_planning`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/app/update.rs crates/aim-core/src/update/ranking.rs crates/aim-core/tests/update_planning.rs crates/aim-cli/tests/end_to_end_cli.rs
git commit -m "feat: add update fallback channel behavior"
```
### Task 12: Remove or slim the legacy GitHub adapter entry points
**Files:**
- Modify: `crates/aim-core/src/adapters/mod.rs`
- Modify: `crates/aim-core/src/adapters/github.rs`
- Modify: `crates/aim-core/src/adapters/traits.rs`
- Modify: `crates/aim-core/tests/adapter_contract.rs`
- Modify: `crates/aim-core/tests/adapter_smoke.rs`
**Step 1: Write the failing test**
```rust
use aim_core::adapters::github::GitHubAdapter;
#[test]
fn legacy_github_adapter_delegates_to_source_pipeline() {
let adapter = GitHubAdapter::default();
let result = adapter.normalize("sharkdp/bat").unwrap();
assert_eq!(result.normalized_kind.as_str(), "github-repository");
}
```
**Step 2: Run test to verify it fails**
Run: `cargo test legacy_github_adapter_delegates_to_source_pipeline --package aim-core --test adapter_contract`
Expected: FAIL because the legacy adapter layer has not been reconciled with the new boundaries
**Step 3: Write minimal implementation**
Either:
- slim the GitHub adapter into a compatibility wrapper over `source::github`, or
- reduce the adapter layer so it no longer owns metadata or ranking responsibilities
Do not leave duplicated GitHub logic in both places.
**Step 4: Run test to verify it passes**
Run: `cargo test legacy_github_adapter_delegates_to_source_pipeline --package aim-core --test adapter_contract`
Expected: PASS
**Step 5: Commit**
```bash
git add crates/aim-core/src/adapters/mod.rs crates/aim-core/src/adapters/github.rs crates/aim-core/src/adapters/traits.rs crates/aim-core/tests/adapter_contract.rs crates/aim-core/tests/adapter_smoke.rs
git commit -m "refactor: reconcile legacy adapter layer with source pipeline"
```
### Task 13: Run full verification and update top-level docs if needed
**Files:**
- Modify: `README.md`
- Modify: `.plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-design.md`
- Modify: `.plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-implementation-plan.md`
**Step 1: Run focused test suites**
Run: `cargo test --package aim-core --test query_resolution --test identity_resolution --test github_source_discovery --test metadata_contract --test metadata_electron_builder --test metadata_zsync --test github_add_flow --test update_planning --test registry_roundtrip`
Expected: PASS
**Step 2: Run full workspace verification**
Run: `cargo test --workspace`
Expected: PASS
Run: `cargo fmt --check`
Expected: PASS
Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings`
Expected: PASS
**Step 3: Update docs minimally**
Document any visible changes to supported GitHub input forms or update behavior in `README.md`. Only update the design or plan docs if implementation forced a justified divergence.
**Step 4: Re-run doc-relevant tests if docs changed code examples**
Run: `cargo test --workspace`
Expected: PASS
**Step 5: Commit**
```bash
git add README.md .plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-design.md .plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-implementation-plan.md
git commit -m "docs: finalize github source end-to-end support"
```