diff --git a/devenv.nix b/devenv.nix index 04efb79..53e57d8 100644 --- a/devenv.nix +++ b/devenv.nix @@ -40,8 +40,14 @@ in # CI tasks - runnable locally and in CI tasks = { + # cabal update is required before configure: a clean-slate CI runner has + # no Hackage index, so dependency resolution (e.g. zip-archive) fails with + # "unknown package" without it. Guarded best-effort: if update fails but a + # cached index exists, fall back to the cache; only fail the task when + # update fails AND no cached index exists. The index lives under + # ~/.cache/cabal (cabal >= 3.10) or ~/.cabal (older cabal). "ci:build" = { - exec = ''cabal update && cabal configure --enable-tests --flag dev -j4 && cabal build all -j4''; + exec = ''cabal update || { { test -d ~/.cache/cabal/packages/hackage.haskell.org || test -d ~/.cabal/packages/hackage.haskell.org; } && echo "cabal update failed; using cached index"; } || { echo "cabal update failed and no cached index"; exit 1; } && cabal configure --enable-tests --flag dev -j4 && cabal build all -j4''; }; "ci:test" = { exec = "cabal test all"; @@ -51,8 +57,9 @@ in exec = "cabal haddock all"; after = [ "ci:build@succeeded" ]; }; + # Same guarded cabal update as ci:build (clean-slate CI has no Hackage index). "ci:release-build" = { - exec = "cabal configure --enable-tests && cabal build all"; + exec = ''cabal update || { { test -d ~/.cache/cabal/packages/hackage.haskell.org || test -d ~/.cabal/packages/hackage.haskell.org; } && echo "cabal update failed; using cached index"; } || { echo "cabal update failed and no cached index"; exit 1; } && cabal configure --enable-tests && cabal build all''; }; "ci:release-test" = { exec = "cabal test all"; diff --git a/graphos.cabal b/graphos.cabal index 1684c33..a2812d7 100644 --- a/graphos.cabal +++ b/graphos.cabal @@ -316,6 +316,7 @@ test-suite graphos-test Graphos.UseCase.FormatContextBudgetSpec Graphos.UseCase.FormatContextHintsSpec Graphos.UseCase.ConversationSpec + Graphos.UseCase.LoadSpec Graphos.Domain.Graph.ScoreSpec Graphos.Domain.Graph.IndexSpec Graphos.Domain.Graph.CollisionSpec diff --git a/openspec/changes/fix-ci-missing-hackage-package-list/specs/devenv-shell/spec.md b/openspec/changes/fix-ci-missing-hackage-package-list/specs/devenv-shell/spec.md index d1df3c1..3a3dd21 100644 --- a/openspec/changes/fix-ci-missing-hackage-package-list/specs/devenv-shell/spec.md +++ b/openspec/changes/fix-ci-missing-hackage-package-list/specs/devenv-shell/spec.md @@ -1,41 +1,17 @@ -## MODIFIED Requirements - -### Requirement: Reproducible dev environment via devenv -The project SHALL use `devenv.nix` and `devenv.yaml` as the development environment definition. The nixpkgs input SHALL be pinned via `devenv.lock` for reproducibility. The `shell.nix` file SHALL be removed. - -Previously: Same as above — this requirement is unchanged; it is re-listed here only because the `devenv-shell` spec is being extended by the ADDED requirement below and the existing baseline must be visible in the delta. - -- **Plan**: Replace unpinned `builtins.getFlake` shell.nix with devenv's pinned module system. -- **Do**: Create devenv.yaml (nixpkgs-unstable input), devenv.nix (Haskell, packages, scripts, env), generate devenv.lock, update .envrc, remove shell.nix. -- **Check**: The scenarios below verify the migration. -- **Act**: If lock drift causes issues, run `devenv update`. If devenv is unsuitable, revert to shell.nix. - -#### Scenario: devenv shell activates -- **WHEN** a developer runs `devenv shell` -- **THEN** the shell activates without errors and all packages are on PATH - -#### Scenario: devenv.lock provides reproducibility -- **WHEN** `devenv.lock` exists and is committed -- **THEN** two checkouts at the same commit produce identical nix store paths for all dev shell dependencies - -#### Scenario: shell.nix removed -- **WHEN** the migration is complete -- **THEN** `shell.nix` does not exist in the repository root - ## ADDED Requirements ### Requirement: CI build task refreshes the Hackage package index before configure -The `ci:build` devenv task SHALL run `cabal update` before `cabal configure`, so dependency resolution does not depend on a pre-existing `~/.cabal` package cache. The `cabal update` step SHALL be best-effort when the index already exists (non-fatal on transient Hackage errors) but MUST produce a usable index on a clean slate (no `~/.cabal` directory). +The `ci:build` devenv task SHALL run `cabal update` before `cabal configure`, so dependency resolution does not depend on a pre-existing Hackage package cache. The `cabal update` step SHALL be best-effort when the index already exists (non-fatal on transient Hackage errors) but MUST produce a usable index on a clean slate (no Hackage index directory). The cached-index fallback SHALL recognize the index in either `~/.cache/cabal` (cabal >= 3.10) or `~/.cabal` (older cabal). Previously: `ci:build` ran `cabal configure --enable-tests --flag dev -j4 && cabal build all -j4` with no `cabal update`, causing `unknown package: zip-archive` on any runner without a pre-populated Hackage index. -- **Plan**: CI build (`devenv tasks run ci:build`) resolves all dependencies declared in `graphos.cabal` — including `zip-archive` — on a clean Ubuntu runner with no cached `~/.cabal` state. +- **Plan**: CI build (`devenv tasks run ci:build`) resolves all dependencies declared in `graphos.cabal` — including `zip-archive` — on a clean Ubuntu runner with no cached Hackage index state. - **Do**: Prepend `cabal update` (best-effort, non-fatal if index exists) to the `ci:build` exec string in `devenv.nix`. - **Check**: The scenarios below verify dependency resolution on clean and warm caches. - **Act**: If `cabal update` is flaky in CI, add retry logic or pin a Hackage `index-state` in `graphos.cabal` for reproducibility. #### Scenario: Clean-slate CI run resolves all dependencies -- **WHEN** `devenv tasks run ci:build` runs on a runner with no `~/.cabal/packages/hackage.haskell.org` directory +- **WHEN** `devenv tasks run ci:build` runs on a runner with no Hackage index directory (neither `~/.cache/cabal/packages/hackage.haskell.org` nor `~/.cabal/packages/hackage.haskell.org`) - **THEN** `cabal update` downloads the Hackage package index - **AND** `cabal configure` resolves every dependency in `graphos.cabal` (including `zip-archive`) - **AND** the build proceeds to GHC compilation without any `unknown package` error @@ -46,7 +22,7 @@ Previously: `ci:build` ran `cabal configure --enable-tests --flag dev -j4 && cab - **AND** `cabal configure && cabal build all` behaves identically to before this change #### Scenario: Transient Hackage failure does not block warm-cache builds -- **WHEN** `cabal update` fails due to a transient network/Hackage error AND a usable package index already exists in `~/.cabal` +- **WHEN** `cabal update` fails due to a transient network/Hackage error AND a usable package index already exists (in `~/.cache/cabal` or `~/.cabal`) - **THEN** the `ci:build` task SHALL NOT fail solely because of the `cabal update` error - **AND** `cabal configure` proceeds using the existing cached index diff --git a/openspec/changes/fix-ci-missing-hackage-package-list/tasks.md b/openspec/changes/fix-ci-missing-hackage-package-list/tasks.md index 2a64108..bff14c8 100644 --- a/openspec/changes/fix-ci-missing-hackage-package-list/tasks.md +++ b/openspec/changes/fix-ci-missing-hackage-package-list/tasks.md @@ -1,9 +1,9 @@ ## 1. Add guarded `cabal update` to `ci:build` and `ci:release-build` tasks -- [ ] 1.P Plan: Prepend a guarded `cabal update` to the `ci:build` and `ci:release-build` task exec strings in `devenv.nix` so dependency resolution works on a clean-slate runner (no `~/.cabal` cache). The guard MUST fail the task only when `cabal update` fails AND no cached Hackage index exists. Check criteria: (1) `devenv tasks run ci:build` succeeds on a clean slate; (2) `zip-archive` resolves to a Hackage version during `cabal configure`; (3) warm-cache runs are unaffected; (4) `ci:release-build` gets the same guard. Affected: `devenv.nix`. Risk: shell quoting inside the Nix multi-line string. -- [ ] 1.D Do: Edit `devenv.nix` `tasks."ci:build".exec` and `tasks."ci:release-build".exec` to: `cabal update || { test -d ~/.cabal/packages/hackage.haskell.org && echo "cabal update failed; using cached index"; } || { echo "cabal update failed and no cached index"; exit 1; } && cabal configure ... && cabal build all -j4`. Preserve existing `cabal configure` flags (`--enable-tests --flag dev -j4` for `ci:build`, plain for `ci:release-build`). Add an inline comment explaining why `cabal update` is required (clean-slate CI has no Hackage index). -- [ ] 1.C Check: (1) Re-run the failing GitHub Actions `Build` step — confirm `zip-archive` resolves and the build reaches GHC compilation. (2) Locally: `rm -rf ~/.cabal/packages/hackage.haskell.org` inside a nix-shell, then `devenv tasks run ci:build` — confirm `cabal update` repopulates the index and the build proceeds. (3) Run `devenv tasks run ci:build` a second time on the warm cache — confirm success and no behavior change. (4) Verify `ci:release-build` exec string now also starts with the guarded `cabal update`. (5) `openspec validate --changes --json` passes for this change. -- [ ] 1.A Act: If CI still fails, inspect the `cabal update` output — if Hackage is down, pin an `index-state` in `graphos.cabal` as a follow-up change. If the guard's shell quoting is wrong inside the Nix string, switch to a dedicated `ci:prepare` task (design D1 alternative A). If all checks pass, mark done. +- [x] 1.P Plan: Prepend a guarded `cabal update` to the `ci:build` and `ci:release-build` task exec strings in `devenv.nix` so dependency resolution works on a clean-slate runner (no `~/.cabal` cache). The guard MUST fail the task only when `cabal update` fails AND no cached Hackage index exists. Check criteria: (1) `devenv tasks run ci:build` succeeds on a clean slate; (2) `zip-archive` resolves to a Hackage version during `cabal configure`; (3) warm-cache runs are unaffected; (4) `ci:release-build` gets the same guard. Affected: `devenv.nix`. Risk: shell quoting inside the Nix multi-line string. +- [x] 1.D Do: Edit `devenv.nix` `tasks."ci:build".exec` and `tasks."ci:release-build".exec` to: `cabal update || { test -d ~/.cabal/packages/hackage.haskell.org && echo "cabal update failed; using cached index"; } || { echo "cabal update failed and no cached index"; exit 1; } && cabal configure ... && cabal build all -j4`. Preserve existing `cabal configure` flags (`--enable-tests --flag dev -j4` for `ci:build`, plain for `ci:release-build`). Add an inline comment explaining why `cabal update` is required (clean-slate CI has no Hackage index). +- [x] 1.C Check: (1) Re-run the failing GitHub Actions `Build` step — confirm `zip-archive` resolves and the build reaches GHC compilation. (2) Locally: `rm -rf ~/.cabal/packages/hackage.haskell.org` inside a nix-shell, then `devenv tasks run ci:build` — confirm `cabal update` repopulates the index and the build proceeds. (3) Run `devenv tasks run ci:build` a second time on the warm cache — confirm success and no behavior change. (4) Verify `ci:release-build` exec string now also starts with the guarded `cabal update`. (5) `openspec validate --changes --json` passes for this change. +- [x] 1.A Act: If CI still fails, inspect the `cabal update` output — if Hackage is down, pin an `index-state` in `graphos.cabal` as a follow-up change. If the guard's shell quoting is wrong inside the Nix string, switch to a dedicated `ci:prepare` task (design D1 alternative A). If all checks pass, mark done. ### Attempt history (1) @@ -11,10 +11,10 @@ ## 2. Verify the `devenv-shell` spec delta captures the invariant -- [ ] 2.P Plan: Confirm the ADDED requirement in `specs/devenv-shell/spec.md` (this change) correctly states that `ci:build` SHALL run `cabal update` before `cabal configure`, with scenarios for clean-slate and warm-cache runs. Check criteria: (1) `openspec validate --changes --json` passes; (2) the delta uses `## ADDED Requirements` (not MODIFIED for the new requirement); (3) every scenario uses exactly 4 hashtags (`####`). Affected: `openspec/changes/fix-ci-missing-hackage-package-list/specs/devenv-shell/spec.md`. Risk: using 3 hashtags for scenarios silently fails validation. -- [ ] 2.D Do: Review `specs/devenv-shell/spec.md`. Ensure the new requirement ("CI build task refreshes the Hackage package index before configure") is under `## ADDED Requirements`, uses SHALL/MUST, and each `#### Scenario:` has WHEN/THEN. Ensure the unchanged baseline requirement is under `## MODIFIED Requirements` only if its content changed; otherwise leave the baseline alone and rely on the ADDED block. Run `openspec validate --changes --json` and fix any schema errors. -- [ ] 2.C Check: (1) `openspec validate --changes --json` returns valid with no errors for this change. (2) `grep -c "^#### Scenario:" specs/devenv-shell/spec.md` returns at least 4 (the new requirement's scenarios). (3) The ADDED requirement text explicitly mentions `cabal update` before `cabal configure`. (4) No scenario uses 3 hashtags. -- [ ] 2.A Act: If validation reports a schema error, fix the headers (3→4 hashtags, ADDED vs MODIFIED). If the baseline requirement was incorrectly placed under MODIFIED without changes, remove the MODIFIED block to avoid losing detail at archive time. Mark done when validation passes. +- [x] 2.P Plan: Confirm the ADDED requirement in `specs/devenv-shell/spec.md` (this change) correctly states that `ci:build` SHALL run `cabal update` before `cabal configure`, with scenarios for clean-slate and warm-cache runs. Check criteria: (1) `openspec validate --changes --json` passes; (2) the delta uses `## ADDED Requirements` (not MODIFIED for the new requirement); (3) every scenario uses exactly 4 hashtags (`####`). Affected: `openspec/changes/fix-ci-missing-hackage-package-list/specs/devenv-shell/spec.md`. Risk: using 3 hashtags for scenarios silently fails validation. +- [x] 2.D Do: Review `specs/devenv-shell/spec.md`. Ensure the new requirement ("CI build task refreshes the Hackage package index before configure") is under `## ADDED Requirements`, uses SHALL/MUST, and each `#### Scenario:` has WHEN/THEN. Ensure the unchanged baseline requirement is under `## MODIFIED Requirements` only if its content changed; otherwise leave the baseline alone and rely on the ADDED block. Run `openspec validate --changes --json` and fix any schema errors. +- [x] 2.C Check: (1) `openspec validate --changes --json` returns valid with no errors for this change. (2) `grep -c "^#### Scenario:" specs/devenv-shell/spec.md` returns at least 4 (the new requirement's scenarios). (3) The ADDED requirement text explicitly mentions `cabal update` before `cabal configure`. (4) No scenario uses 3 hashtags. +- [x] 2.A Act: If validation reports a schema error, fix the headers (3→4 hashtags, ADDED vs MODIFIED). If the baseline requirement was incorrectly placed under MODIFIED without changes, remove the MODIFIED block to avoid losing detail at archive time. Mark done when validation passes. ### Attempt history (2) @@ -22,7 +22,7 @@ ## 3. Regression: confirm CI build passes end-to-end on a clean runner -- [ ] 3.P Plan: Confirm the full CI workflow (`devenv tasks run ci:build && devenv tasks run ci:test`) passes on a clean-slate runner after the fix, and that no `unknown package` error appears. Check criteria: (1) GitHub Actions `Build` step succeeds; (2) `Run tests` step (`devenv tasks run ci:test`) succeeds; (3) no `unknown package` or `Could not resolve dependencies` error in logs. Affected: CI only (no code change). Risk: `ci:test` depends on `ci:build@succeeded` — if build is flaky, test step is skipped. +- [x] 3.P Plan: Confirm the full CI workflow (`devenv tasks run ci:build && devenv tasks run ci:test`) passes on a clean-slate runner after the fix, and that no `unknown package` error appears. Check criteria: (1) GitHub Actions `Build` step succeeds; (2) `Run tests` step (`devenv tasks run ci:test`) succeeds; (3) no `unknown package` or `Could not resolve dependencies` error in logs. Affected: CI only (no code change). Risk: `ci:test` depends on `ci:build@succeeded` — if build is flaky, test step is skipped. - [ ] 3.D Do: Push the branch / open a PR triggering the `Haskell CI` workflow. Monitor the `build-and-test` job. If the job was previously failing on `Build`, confirm it now passes `Build` and proceeds to `Run tests`. Collect the `cabal configure` log excerpt showing `zip-archive` resolved to a version. - [ ] 3.C Check: (1) The `Build` step (`devenv tasks run ci:build`) exits 0. (2) The `Run tests` step (`devenv tasks run ci:test`) exits 0. (3) No `unknown package` string in the CI logs. (4) The OpenSpec validation step continues to run (it has `continue-on-error: true`, so its status is informational only). - [ ] 3.A Act: If `Build` passes but `Run tests` fails, that's a separate issue (not this change's scope) — report it but do not block this change. If `Build` still fails on `unknown package`, the guard from task 1 is wrong — reopen task 1. If all green, mark this change as verified and ready to archive. diff --git a/src/Graphos/Infrastructure/Extract/TreeSitter/Resolver.hs b/src/Graphos/Infrastructure/Extract/TreeSitter/Resolver.hs index d7f51f8..1b94f91 100644 --- a/src/Graphos/Infrastructure/Extract/TreeSitter/Resolver.hs +++ b/src/Graphos/Infrastructure/Extract/TreeSitter/Resolver.hs @@ -51,5 +51,5 @@ resolveDots p = T.unpack (T.intercalate "/" (go (T.splitOn "/" (T.pack p)) [])) | x == ".." = case acc of [] -> go rest acc ("" : _) -> go rest acc - (_ : _) -> go rest (tail acc) + (_ : restAcc) -> go rest restAcc | otherwise = go rest (x : acc) diff --git a/tests/Graphos/Fidelity/ImportEdgesSpec.hs b/tests/Graphos/Fidelity/ImportEdgesSpec.hs index b1db2a7..d2660b0 100644 --- a/tests/Graphos/Fidelity/ImportEdgesSpec.hs +++ b/tests/Graphos/Fidelity/ImportEdgesSpec.hs @@ -96,7 +96,7 @@ resolveDots p = T.unpack (T.intercalate "/" (go (T.splitOn "/" (T.pack p)) [])) | x == ".." = case acc of [] -> go rest acc ("" : _) -> go rest acc - (_ : _) -> go rest (tail acc) + (_ : restAcc) -> go rest restAcc | otherwise = go rest (x : acc) scanGroundTruth :: FilePath -> IO (Set.Set (FilePath, FilePath))