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
11 changes: 9 additions & 2 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand 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";
Expand Down
1 change: 1 addition & 0 deletions graphos.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
18 changes: 9 additions & 9 deletions openspec/changes/fix-ci-missing-hackage-package-list/tasks.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
## 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)

<!-- empty unless a retry is needed -->

## 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)

<!-- empty unless a retry is needed -->

## 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.
Expand Down
2 changes: 1 addition & 1 deletion src/Graphos/Infrastructure/Extract/TreeSitter/Resolver.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion tests/Graphos/Fidelity/ImportEdgesSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading