aim/.plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-implementation-plan.md
2026-03-19 20:14:39 +00:00

22 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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"