Skip to content

Commit 17a2010

Browse files
Napper LSP: Phase 1-2 scaffold, shared features, 14 integration tests (#8)
# TLDR; New `napper-lsp` F# language server (Phase 1-2 only — NO cutover). Replaces duplicated TypeScript parsing logic with shared F# in `Napper.Core`. 14 integration tests over real JSON-RPC stdio. # Details **New Napper.Core modules (shared by CLI and LSP):** - `CurlGenerator.fs` — generates curl commands from `NapRequest` - `SectionScanner.fs` — finds `[section]` header positions for document symbols - `Environment.detectEnvironmentNames` — scans `.napenv.*` files, returns env names **New LSP project (`src/Nap.Lsp/`):** - stdio JSON-RPC transport via `Ionide.LanguageServerProtocol` - `textDocument/documentSymbol` — sections for `.nap` and `.naplist` (replaces `extractHttpMethod`, `parsePlaylistStepPaths` in TS) - `textDocument/codeLens` — request section detection (replaces CodeLens TS parsing) - `workspace/executeCommand`: - `napper.requestInfo` — method + URL + headers (replaces `parseMethodAndUrl` in TS) - `napper.copyCurl` — curl string (replaces curl generation in TS) - `napper.listEnvironments` — env names (replaces `detectEnvironments` in TS) **Specs updated:** LSP-SPEC, LSP-PLAN, IDE-EXTENSION-SPEC, IDE-EXTENSION-PLAN, ZED-EXTENSION-PLAN, Claude.md **No existing code modified.** LSP is a parallel project — cutover is Phase 3 (not started). # How do the tests prove the change works 14 integration tests in `src/Nap.Lsp.Tests/` — each launches the real `napper-lsp` binary as a subprocess and talks JSON-RPC over stdio (the exact same protocol VSCode and Zed use): - initialize handshake returns correct capabilities - initialized notification accepted - didOpen/didChange/didClose document sync - shutdown + exit clean lifecycle - malformed request survives gracefully - unknown method returns LSP error - documentSymbol returns sections for `.nap` file (meta, request, assert) - documentSymbol returns sections for `.naplist` file (meta, steps) - codeLens returns lenses for `.nap` request sections - executeCommand requestInfo returns parsed method + URL - executeCommand copyCurl returns curl string - executeCommand listEnvironments returns env names from `.napenv.*` files --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b60db24 commit 17a2010

290 files changed

Lines changed: 31825 additions & 11631 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/fix-ci/SKILL.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
name: fix-ci
3+
description: Fetches the latest GitHub Actions logs for the current branch's PR, analyzes all failures, and fixes them. Use when CI is red, a PR has failing checks, or the user says "fix ci". Requires an open PR for the current branch.
4+
argument-hint: "[optional job name to focus on]"
5+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
6+
---
7+
8+
# Fix CI
9+
10+
Diagnose and fix all GitHub Actions failures for the current branch's PR.
11+
12+
## Step 1: Validate branch has a PR
13+
14+
```bash
15+
BRANCH=$(git branch --show-current)
16+
PR_JSON=$(gh pr list --head "$BRANCH" --state open --json number,title,url --limit 1)
17+
```
18+
19+
If the JSON array is empty, **stop immediately**:
20+
> No open PR found for branch `$BRANCH`. Create a PR first.
21+
22+
Otherwise extract the PR number and continue.
23+
24+
## Step 2: Fetch failed logs
25+
26+
```bash
27+
PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number')
28+
gh pr checks "$PR_NUMBER"
29+
RUN_ID=$(gh run list --branch "$BRANCH" --limit 1 --json databaseId --jq '.[0].databaseId')
30+
gh run view "$RUN_ID"
31+
gh run view "$RUN_ID" --log-failed
32+
```
33+
34+
Read **every line** of `--log-failed` output. For each failure note the exact file, line, and error message.
35+
36+
If `$ARGUMENTS` specifies a job name, prioritize that job but still report all failures.
37+
38+
## Step 3: Categorize and fix
39+
40+
Work through failures in this order:
41+
42+
1. **Formatting** — run auto-formatters first to clear noise
43+
2. **Compilation errors** — must compile before lint/test
44+
3. **Lint violations** — fix the code pattern
45+
4. **Runtime / test failures** — fix source code to satisfy the test
46+
47+
### Hard constraints
48+
49+
- **NEVER modify test files** — fix the source code, not the tests
50+
- **NEVER add suppressions** (`#[allow(...)]`, `// eslint-disable`, `#pragma warning disable`)
51+
- **NEVER use `any` in TypeScript** to silence type errors
52+
- **NEVER delete or ignore failing tests**
53+
- **NEVER remove assertions**
54+
55+
## Step 4: Loop `make ci` until green
56+
57+
```bash
58+
make ci
59+
```
60+
61+
If it fails: read output, fix the issue (same constraints as Step 3), run again. **Keep looping until a full pass is clean.**
62+
63+
If stuck on the same failure after 5 attempts, ask the user for help.
64+
65+
## Step 5: Commit/Push
66+
67+
Once `make ci` passes:
68+
69+
1. Commit, but DO NOT MARK THE COMMIT WITH YOU AS AN AUTHOR!!!
70+
2. Push
71+
3. Monitor until completion or failure
72+
4. Upon failure, go back to the start of this document

.config/dotnet-tools.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"fantomas": {
6+
"version": "7.0.5",
7+
"commands": [
8+
"fantomas"
9+
],
10+
"rollForward": false
11+
}
12+
}
13+
}

.editorconfig

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.{fs,fsx}]
12+
indent_size = 4
13+
14+
# F# compiler diagnostics — all unused things are errors
15+
dotnet_diagnostic.FS1182.severity = error
16+
17+
# Opt-in warnings elevated to errors
18+
dotnet_diagnostic.FS3388.severity = error
19+
dotnet_diagnostic.FS3389.severity = error
20+
dotnet_diagnostic.FS3390.severity = error
21+
dotnet_diagnostic.FS3391.severity = error
22+
dotnet_diagnostic.FS3395.severity = error
23+
dotnet_diagnostic.FS3559.severity = error
24+
dotnet_diagnostic.FS3560.severity = error
25+
dotnet_diagnostic.FS3582.severity = error
26+
27+
[*.ts]
28+
indent_size = 2
29+
30+
[*.json]
31+
indent_size = 2
32+
33+
[*.{yml,yaml}]
34+
indent_size = 2
35+
36+
[*.rs]
37+
indent_size = 4

.github/workflows/pr.yml

Lines changed: 203 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,33 @@ jobs:
1010
runs-on: ubuntu-latest
1111
defaults:
1212
run:
13-
working-directory: src/Nap.VsCode
13+
working-directory: src/Napper.VsCode
1414
steps:
1515
- uses: actions/checkout@v4
1616

1717
- uses: actions/setup-node@v4
1818
with:
1919
node-version: 22
20+
cache: npm
21+
cache-dependency-path: src/Napper.VsCode/package-lock.json
2022

2123
- uses: actions/setup-dotnet@v4
2224
with:
2325
dotnet-version: "10.0.x"
2426

27+
- name: Cache NuGet packages
28+
uses: actions/cache@v4
29+
with:
30+
path: ~/.nuget/packages
31+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.fsproj') }}
32+
restore-keys: ${{ runner.os }}-nuget-
33+
2534
- name: Install dependencies
2635
run: npm ci
2736

37+
- name: Format check
38+
run: npm run format:check
39+
2840
- name: Lint
2941
run: npm run lint
3042

@@ -34,15 +46,38 @@ jobs:
3446
- name: Unit tests with coverage
3547
run: npm run test:unit
3648

49+
- name: Add CLI to PATH
50+
run: echo "${{ github.workspace }}/src/Napper.VsCode/bin" >> "$GITHUB_PATH"
51+
3752
- name: E2E tests
3853
run: xvfb-run --auto-servernum npm test
3954

55+
- name: Extract TypeScript coverage percentage
56+
id: ts-coverage
57+
run: |
58+
COVERAGE=$(npx c8 report --reporter text 2>/dev/null | grep 'All files' | awk '{print $4}' || echo "0")
59+
echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT"
60+
61+
- name: Check TypeScript coverage threshold
62+
run: |
63+
ACTUAL="${{ steps.ts-coverage.outputs.coverage }}"
64+
THRESHOLD="${{ vars.TS_COVERAGE_THRESHOLD }}"
65+
echo "TypeScript coverage: ${ACTUAL}% (threshold: ${THRESHOLD}%)"
66+
if [ -z "$THRESHOLD" ] || [ "$THRESHOLD" = "0" ]; then
67+
echo "No threshold set — skipping"
68+
exit 0
69+
fi
70+
if (( $(echo "$ACTUAL < $THRESHOLD" | bc -l) )); then
71+
echo "::error::TypeScript coverage ${ACTUAL}% is below threshold ${THRESHOLD}%"
72+
exit 1
73+
fi
74+
4075
- name: Upload TypeScript coverage
4176
if: always()
4277
uses: actions/upload-artifact@v4
4378
with:
4479
name: typescript-coverage
45-
path: coverage/typescript/report/
80+
path: src/Napper.VsCode/coverage/
4681

4782
test-fsharp:
4883
name: F# Build & Tests
@@ -54,17 +89,73 @@ jobs:
5489
with:
5590
dotnet-version: "10.0.x"
5691

92+
- name: Cache NuGet packages
93+
uses: actions/cache@v4
94+
with:
95+
path: ~/.nuget/packages
96+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.fsproj') }}
97+
restore-keys: ${{ runner.os }}-nuget-
98+
5799
- name: Install ReportGenerator
58100
run: dotnet tool install --global dotnet-reportgenerator-globaltool
59101

60102
- name: Install dotnet-script
61103
run: dotnet tool install -g dotnet-script
62104

63-
- name: Build
64-
run: dotnet build --nologo
105+
- name: Restore tools
106+
run: dotnet tool restore
107+
108+
- name: Format check (Fantomas)
109+
run: dotnet fantomas --check src/
110+
111+
- name: Restore
112+
run: dotnet restore
113+
114+
- name: Build (warnings are errors)
115+
run: dotnet build --no-restore --nologo -warnaserror
65116

66117
- name: Test with coverage
67-
run: bash scripts/test-fsharp.sh
118+
run: make test-fsharp
119+
120+
- name: Extract Napper.Core coverage percentage
121+
id: napcore-coverage
122+
run: |
123+
COVERAGE=$(grep -oP 'Line coverage: \K[0-9.]+' coverage/fsharp/report/Summary.txt || echo "0")
124+
echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT"
125+
126+
- name: Check Napper.Core coverage threshold
127+
run: |
128+
ACTUAL="${{ steps.napcore-coverage.outputs.coverage }}"
129+
THRESHOLD="${{ vars.FSHARP_COVERAGE_THRESHOLD }}"
130+
echo "Napper.Core coverage: ${ACTUAL}% (threshold: ${THRESHOLD}%)"
131+
if [ -z "$THRESHOLD" ] || [ "$THRESHOLD" = "0" ]; then
132+
echo "No threshold set — skipping"
133+
exit 0
134+
fi
135+
if (( $(echo "$ACTUAL < $THRESHOLD" | bc -l) )); then
136+
echo "::error::Napper.Core coverage ${ACTUAL}% is below threshold ${THRESHOLD}%"
137+
exit 1
138+
fi
139+
140+
- name: Extract DotHttp coverage percentage
141+
id: dothttp-coverage
142+
run: |
143+
COVERAGE=$(grep -oP 'Line coverage: \K[0-9.]+' coverage/dothttp/report/Summary.txt || echo "0")
144+
echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT"
145+
146+
- name: Check DotHttp coverage threshold
147+
run: |
148+
ACTUAL="${{ steps.dothttp-coverage.outputs.coverage }}"
149+
THRESHOLD="${{ vars.DOTHTTP_COVERAGE_THRESHOLD }}"
150+
echo "DotHttp coverage: ${ACTUAL}% (threshold: ${THRESHOLD}%)"
151+
if [ -z "$THRESHOLD" ] || [ "$THRESHOLD" = "0" ]; then
152+
echo "No threshold set — skipping"
153+
exit 0
154+
fi
155+
if (( $(echo "$ACTUAL < $THRESHOLD" | bc -l) )); then
156+
echo "::error::DotHttp coverage ${ACTUAL}% is below threshold ${THRESHOLD}%"
157+
exit 1
158+
fi
68159
69160
- name: Upload F# coverage
70161
if: always()
@@ -73,6 +164,111 @@ jobs:
73164
name: fsharp-coverage
74165
path: coverage/fsharp/report/
75166

167+
- name: Upload DotHttp coverage
168+
if: always()
169+
uses: actions/upload-artifact@v4
170+
with:
171+
name: dothttp-coverage
172+
path: coverage/dothttp/report/
173+
174+
- name: Extract Napper.Lsp coverage percentage
175+
id: lsp-coverage
176+
run: |
177+
if [ -f coverage/lsp/report/Summary.txt ]; then
178+
COVERAGE=$(grep -oP 'Line coverage: \K[0-9.]+' coverage/lsp/report/Summary.txt || echo "0")
179+
else
180+
COVERAGE="0"
181+
fi
182+
echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT"
183+
184+
- name: Check Napper.Lsp coverage threshold
185+
run: |
186+
ACTUAL="${{ steps.lsp-coverage.outputs.coverage }}"
187+
THRESHOLD="${{ vars.LSP_COVERAGE_THRESHOLD }}"
188+
echo "Napper.Lsp coverage: ${ACTUAL}% (threshold: ${THRESHOLD}%)"
189+
if [ -z "$THRESHOLD" ] || [ "$THRESHOLD" = "0" ]; then
190+
echo "No threshold set — skipping"
191+
exit 0
192+
fi
193+
if [ "$ACTUAL" = "0" ] && grep -q 'Assemblies: 0' coverage/lsp/report/Summary.txt 2>/dev/null; then
194+
echo "LSP tests are integration tests (subprocess) — skipping coverage threshold"
195+
exit 0
196+
fi
197+
if (( $(echo "$ACTUAL < $THRESHOLD" | bc -l) )); then
198+
echo "::error::Napper.Lsp coverage ${ACTUAL}% is below threshold ${THRESHOLD}%"
199+
exit 1
200+
fi
201+
202+
- name: Upload Napper.Lsp coverage
203+
if: always()
204+
uses: actions/upload-artifact@v4
205+
with:
206+
name: lsp-coverage
207+
path: coverage/lsp/report/
208+
209+
test-rust:
210+
name: Rust Build & Tests
211+
runs-on: ubuntu-latest
212+
defaults:
213+
run:
214+
working-directory: src/Napper.Zed
215+
steps:
216+
- uses: actions/checkout@v4
217+
218+
- uses: dtolnay/rust-toolchain@stable
219+
with:
220+
components: clippy, rustfmt
221+
222+
- name: Cache Cargo registry and build
223+
uses: actions/cache@v4
224+
with:
225+
path: |
226+
~/.cargo/registry
227+
~/.cargo/git
228+
src/Napper.Zed/target
229+
key: ${{ runner.os }}-cargo-${{ hashFiles('src/Napper.Zed/Cargo.lock') }}
230+
restore-keys: ${{ runner.os }}-cargo-
231+
232+
- name: Format check
233+
run: cargo fmt -- --check
234+
235+
- name: Clippy
236+
run: cargo clippy
237+
238+
- name: Install cargo-tarpaulin
239+
run: cargo install cargo-tarpaulin
240+
241+
- name: Test with coverage
242+
run: cargo tarpaulin --out xml html --output-dir ../../coverage/rust/report --skip-clean
243+
244+
- name: Extract Rust coverage percentage
245+
id: rust-coverage
246+
run: |
247+
COVERAGE=$(grep -oP 'line-rate="\K[0-9.]+' ../../coverage/rust/report/cobertura.xml 2>/dev/null || echo "0")
248+
COVERAGE_PCT=$(echo "$COVERAGE * 100" | bc -l | xargs printf "%.2f")
249+
echo "coverage=$COVERAGE_PCT" >> "$GITHUB_OUTPUT"
250+
251+
- name: Check Rust coverage threshold
252+
run: |
253+
ACTUAL="${{ steps.rust-coverage.outputs.coverage }}"
254+
THRESHOLD="${{ vars.RUST_COVERAGE_THRESHOLD }}"
255+
echo "Rust coverage: ${ACTUAL}% (threshold: ${THRESHOLD}%)"
256+
if [ -z "$THRESHOLD" ] || [ "$THRESHOLD" = "0" ]; then
257+
echo "No threshold set — skipping"
258+
exit 0
259+
fi
260+
if (( $(echo "$ACTUAL < $THRESHOLD" | bc -l) )); then
261+
echo "::error::Rust coverage ${ACTUAL}% is below threshold ${THRESHOLD}%"
262+
exit 1
263+
fi
264+
265+
- name: Upload Rust coverage
266+
if: always()
267+
uses: actions/upload-artifact@v4
268+
with:
269+
name: rust-coverage
270+
path: coverage/rust/report/
271+
76272
build-website:
77273
name: Website Build
78274
runs-on: ubuntu-latest
@@ -85,6 +281,8 @@ jobs:
85281
- uses: actions/setup-node@v4
86282
with:
87283
node-version: 22
284+
cache: npm
285+
cache-dependency-path: website/package-lock.json
88286

89287
- name: Install dependencies
90288
run: npm ci

0 commit comments

Comments
 (0)