Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c63e332
Fix the open v0.22.0 review findings: parenthesized C# project names,…
MelbourneDeveloper Oct 18, 2026
238067d
CI: fail a Dependabot PR to main, whose skipped pipeline ran nothing
MelbourneDeveloper Oct 18, 2026
fc09827
fixes
MelbourneDeveloper Oct 18, 2026
fd991de
fixes
MelbourneDeveloper Oct 18, 2026
4739586
Pin lspkit at the F# profiler fixes: busy threads only, arrays never …
MelbourneDeveloper Oct 18, 2026
da6911d
Pin lspkit at main's F# profiler fixes (Nimblesite/lspkit#12): busy t…
MelbourneDeveloper Oct 18, 2026
70db4f2
fixes
MelbourneDeveloper Oct 18, 2026
fb3a882
Pin lspkit at the objects-summary and FSharp.Core anchor fixes
MelbourneDeveloper Oct 18, 2026
b2f8948
Format the profiler test kits and drop non-null assertions their asse…
MelbourneDeveloper Oct 18, 2026
211b3a9
Share the branded-panel and dashboard-snapshot test helpers
MelbourneDeveloper Oct 18, 2026
2ec7b5f
Format the F# soak suites and the process-explorer test with Prettier
MelbourneDeveloper Oct 18, 2026
7070396
Land the F# profiler suites' final shape, under 500 lines a file
MelbourneDeveloper Oct 18, 2026
4c29e9c
Pin lspkit at main's F# memory fixes (Nimblesite/lspkit#13, #14)
MelbourneDeveloper Oct 18, 2026
c4f4d27
Refuse a profiler command whose arguments are not one params object
MelbourneDeveloper Oct 18, 2026
4da2a5a
Profile F# in the docs with a narrated walkthrough, and make the film…
MelbourneDeveloper Oct 18, 2026
75b75b5
Pin lspkit at main's profiler fixes (Nimblesite/lspkit#15)
MelbourneDeveloper Oct 18, 2026
d8ccb5e
Tighten the F# profiler suites around the fixed sessions and summaries
MelbourneDeveloper Oct 18, 2026
4e48725
Open the F# CPU truth test's Stopwatch window once samples flow
MelbourneDeveloper Oct 18, 2026
3c7cfe4
Record the F# profiler test bed and the defects it found in the profi…
MelbourneDeveloper Oct 18, 2026
31f3770
Send cpu.list its params object from the profiler suites that called …
MelbourneDeveloper Oct 18, 2026
e11cc0f
Pin lspkit at main's progress log fix (Nimblesite/lspkit#16)
MelbourneDeveloper Oct 18, 2026
87f2da7
Run a C# project's source generators when F# compiles it in memory
MelbourneDeveloper Oct 18, 2026
d03507f
Restore a real repository without freezing the extension host
MelbourneDeveloper Oct 18, 2026
a8971c9
Run source generators without byrefs, and never let one fail a load
MelbourneDeveloper Oct 18, 2026
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
2 changes: 1 addition & 1 deletion .agents/skills/submit-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: submit-pr
description: Creates a pull request with a well-structured description after verifying CI passes. Use when the user asks to submit, create, or open a pull request.
disable-model-invocation: true
disable-model-invocation: false
---
<!-- agent-pmo:a72c926 -->

Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,9 @@ jobs:
#
# `always()` so it reports a real conclusion rather than inheriting a skip:
# on a docs-only PR every upstream job skips and this passes; on a Dependabot
# PR the whole pipeline skips and this passes; any failure or cancellation
# anywhere upstream fails it, and so does a skipped build or test job when
# code changed. Either way the merge is blocked.
# PR the whole pipeline skips and this FAILS, since it ran nothing; any
# failure or cancellation anywhere upstream fails it, and so does a skipped
# build or test job when code changed. Either way the merge is blocked.
ci:
name: CI
if: ${{ always() }}
Expand Down Expand Up @@ -338,6 +338,15 @@ jobs:
run: |-
echo "::error::Code changed but a build or test job was skipped. Check its if: condition."
exit 1
# The whole pipeline skipped this Dependabot PR, so a pass would be a
# pass of nothing. Its bump reaches `main` only through the tested
# consolidation PR; the sweep retires this one. [DIST-CI-DEPENDABOT]
- name: Fail a Dependabot PR to main, which ran no job
if: ${{ github.actor == 'dependabot[bot]' }}
shell: bash
run: |-
echo "::error::Dependabot PRs run no CI. dependabot-automerge.yml sweeps this bump into dependabot-upgrades; it reaches main through the tested dependabot-upgrades -> main PR."
exit 1
- name: Report
shell: bash
run: echo "All CI jobs passed or were skipped."
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
# sidecars ONCE and emitting that platform's VSIX and its standalone server
# archive ([DIST-ARCHIVE]); VERSION is optional and defaults to 0.0.0. Then
# `_print-publish-commands`. Also private: `_screenshots`, `_website-build`,
# `_website-dev`, `_test-website` and `_uninstall-dotnet-10`.
# `_website-dev`, `_test-website`, `_film` and `_uninstall-dotnet-10`.

# ── OS detection ──────────────────────────────────────────────────
# All recipes assume a POSIX shell. On Windows we use Git Bash (bundled with
Expand Down Expand Up @@ -145,7 +145,7 @@ KOVER_PERCENT = $(if $(COVERAGE_TOOLS_PREBUILT),$(DOTNET) target/coverage-tools/
.PHONY: build ci test lint fmt clean audit setup _build-profiler-ui \
reinstall-vsix install-vsix uninstall-vsix install-dotnet-10

.PHONY: _screenshots _website-build _website-dev _uninstall-dotnet-10 \
.PHONY: _screenshots _website-build _website-dev _film _uninstall-dotnet-10 \
_package-vsix-linux-x64 _package-vsix-linux-arm64 \
_package-vsix-darwin-arm64 _package-vsix-darwin-x64 \
_package-vsix-win32-x64 _package-vsix-win32-arm64 \
Expand Down Expand Up @@ -805,6 +805,16 @@ _website-dev:
@echo "==> Starting website development server..."
npm run dev --prefix src/website

# [WEB-DESIGN-MEDIA] Shoot and publish one website film from the packaged VSIX:
# `make _film FILM=fsharp`, or `PUBLISH=1` to publish the last shoot again.
# The films live in src/website/tools/profiler-film/films; its README says how.
_film:
@test -n "$(FILM)" || { echo "usage: make _film FILM=<film> [PUBLISH=1]" >&2; exit 1; }
$(if $(PUBLISH),,$(MAKE) _build-vsix VSIX_PREBUILT=1)
@# Install once: npm ci would wipe the voice model kokoro-js caches in node_modules.
test -d src/website/tools/profiler-film/node_modules || npm ci --prefix src/website/tools/profiler-film
node src/website/tools/profiler-film/film.mjs $(FILM) $(if $(PUBLISH),--publish,)

_test-website:
@echo "==> Running website Playwright tests..."
npm ci --prefix src/website
Expand Down
17 changes: 17 additions & 0 deletions docs/plans/PROFILER-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ These are SharpLsp's shipped profiler features. Cutover is blocked until each ro
- The Rust `profiler_shared` tests raced: each nextest process rewrote the engine beside the shared server binary while another test's engine started, and lost the race with "exited before attaching". The engine is now installed atomically.
- The suites' lifecycle and flows live once in `profiler-kit.ts` (`profilingSuite`, `profileFromRowFor`, `stopTracking`, `snapshotSession`, `retentionGraph`). The Deslop gate is ratcheted from 6.73 % to 6.65 %.
- [x] The `profiler` chunk on macOS (`make _test-vsix-shard CHUNK=profiler`, 2026-10-15): **294 passing, 0 failing**, after the defects below were fixed; the debugger, manifest and configuration suites they touch pass too (30 passing).
- [x] F# on real programs: `FsPricingEngine` (CPU: CSV parsing, discount rules, fuzzy search, routing, reports) and `FsOrderLedger` (an event handler, a memoize cache, a mailbox audit trail, undo history and rejection capture, each rooted at its own module value), under `src/sharplsp/tests/fixtures/fsharp-profiling/`. The suites `profiler-fsharp-cpu-e2e`, `-memory-e2e`, `-retention-e2e`, `-corner-e2e`, `-soak-e2e`, `-soak-cpu-e2e` and `profiler-lsp-protocol-e2e`, `-cpu-e2e`, `-memory-e2e` drive them through the UI and over the wire. The defects they found are fixed in lspkit and the host:
- idle samples counted as active, so a main thread waiting on stdin took half of every profile (lspkit#12);
- a frame every thread shares counted once per thread, so its samples outnumbered the ticks (lspkit#12);
- naming a runtime that cannot profile a busy PID answered `ALREADY_PROFILING`, not `UNSUPPORTED_RUNTIME` (lspkit#12);
- a `ResizeArray<Order>`'s `Order[]` painted the line that builds an `Order` (lspkit#12);
- a retention graph that left instances out still said it was complete (lspkit#12);
- a memory step on a program that had exited answered `OTHER`, not `PROCESS_NOT_FOUND` (lspkit#12);
- a second diff kept the first diff's leak badges (lspkit#12);
- FSharp.Core types were placed at its build server's paths, which do not exist here (lspkit#13);
- `objectsByType` dropped the requested type from its summary when it ranked below the twenty largest, as F# union cases do (lspkit#13);
- the retention graph never drew a root's label, so a module value's `static sessions` looked like any holder (lspkit#14);
- the timeline's peak fell after a release, because `peak_bytes` was each snapshot's own heap size (lspkit#15);
- a heap snapshot taken during a CPU session named types by bare address, because the CPU session enabled the runtime provider (lspkit#15);
- a hot function with no location reached the UI as file `""`, line 0 (lspkit#15);
- the UI's Stop never sent `memory.stop`, so the server kept the session and its graphs and still answered steps (lspkit#15);
- a timed CPU session never left `cpu.list` once its duration elapsed (lspkit#15);
- the host accepted a profiler command with no argument, two arguments or a non-object argument (`profiler/host.rs`).
- [ ] Assertions of the deleted suites with no replacement: counters webview and counter sessions, `openTrace`/`convertTrace` of a `.nettrace`, dump collection with cancellable progress, `inspectObject`, `diffSnapshots`/`detectLeaks` panels over dumps, graph browser interactions. Each maps to a parity gap above.

### Documentation and cutover
Expand Down
12 changes: 12 additions & 0 deletions docs/reviews/V0.22.0-STATIC-REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ The .NET Framework and multi-targeting feature has been implemented across the R

The spec explicitly excludes debugging the desktop .NET Framework CLR: Debug of a Framework-only target is refused, while Run uses the desktop CLR on Windows. “Finished” must be judged against that stated scope, not against availability of a .NET Framework debugger.

## Resolution

Re-checked against `main` on 2026-10-18 (`817d359f`):

| Finding | Status |
|---|---|
| Parenthesized C# project name read as a framework | Fixed (#382). `FrameworkOf` accepts only the `(tfm)` suffix past the project file's stem. Covered by a real single-target `Utility(net48).csproj` and a multi-targeted `Tool(net10.0).csproj` in `WorkspaceManagerTargetFrameworkTests`. |
| Switch announced before invalidation and diagnostics | Fixed (#383). The host clears `nav_cache`, publishes every open document's diagnostics and waits for each, then announces and asks for refreshes. `test_full_stack_target_framework_switch` asserts that order, using a warning only `net10.0` compiles. |
| Run/F5 project paths compared with the wrong case rule | Already fixed before this re-check: `chosenFramework` uses `samePath` from `paths.ts`, and `run-debug-target.test.ts` covers the explicit-project, differently cased focused project on Linux for C# and F#. |
| Explicit Windows-only selection passes with zero tests | Fixed (#384). Off Windows, a `MOCHA_FILES` pattern that names only `windowsOnly` suites fails with the platform reason (`test-runner-selection.test.ts`). |
| Dependabot skips `detect-changes`, and the terminal gate passes the skipped run | Fixed. The skip is intended: bumps are tested once, at the `dependabot-upgrades → main` consolidation PR. But `CI` now FAILS a Dependabot PR to `main`, so a security bump the sweep did not retire cannot merge untested ([DIST-CI-DEPENDABOT], `security-gates.test.mjs`). |

## Findings, in priority order

### [P2] A single-target C# project name can be mistaken for a framework
Expand Down
7 changes: 4 additions & 3 deletions docs/specs/DESIGN-SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,18 @@ Page-level grids and marketing card styles must not leak into `.prose`. Do not r

Screenshots and video of a SharpLsp feature show the real extension, never a mock-up.

- **Capture.** The profiler media is shot by `src/website/tools/profiler-film/`. `capture.mjs` drives an isolated VS Code (its own user-data and extensions directories, never the user's editor) running the packaged VSIX against a real .NET app, and records stills and real-time screencasts. `render.mjs` turns them into the site's assets.
- **Capture.** The profiler media is shot by `src/website/tools/profiler-film/` (its README says how). Every shot runs on `kit/set.mjs`'s `onSet`: an isolated VS Code (its own user-data and extensions directories, never the user's editor) running the packaged VSIX against a real .NET demo app from `demos/`. A walkthrough is one file in `films/`, a list of scenes that `film.mjs` shoots as real-time clips and publishes; `kit/acts/` tells the CPU and memory stories, so each film supplies only its words, its demo and its fix. `films/leak.mjs` films the C# Storefront demo and `films/fsharp.mjs` its F# twin, FsStorefront, so every F# claim the docs make about the profiler is shown on F# code. The composited one-minute film is `films/profiler/`: `capture.mjs` records stills and screencasts, and `render.mjs` turns them into the site's assets.
- **Honesty.** Footage plays in real time unless the frame says otherwise: every fast-forwarded stretch carries an on-screen speed marker, and captions name time-lapses. Motion graphics (title cards, camera moves, highlights, the cursor) frame the footage but never alter what the editor shows.
- **Privacy.** No frame may show a user name, home directory, hostname or terminal. The demo app runs from a neutral temporary path and the terminal is never opened.
- **Formats.**
- Films ship as H.264 MP4 and VP9 WebM with a WebP poster.
- Stills ship as WebP, 2400px wide, with `width` and `height` set.
- Marketing pages may autoplay a film only when it is muted and looping; docs pages use controls.
- Marketing pages may autoplay a film only when it is muted and looping; docs pages use controls and are never muted.
- Every autoplaying video pauses and shows its controls under `prefers-reduced-motion: reduce`.
- Every video ships a WebP poster, the films and the recordings alike.
- **Narration.** A walkthrough may be narrated (`kit/sound/`). Its reviewed script (`films/<film>.narration.json`) is spoken by Kokoro-82M through `kokoro-js` on the capturing machine, never by a speech service or the operating system's voices, whose licences forbid publishing. Each line starts just after its scene's cut, and a scene holds its last frame until its line ends. The music is an original score synthesised from code (`kit/sound/score.mjs`), so no film carries licensed or sampled music, and it ducks under the voice. A narrated film ships AAC in its MP4 and Opus in its WebM, plays unmuted with controls, and carries WebVTT captions in every site language (`<id>.<lang>.vtt`, the page's own language shown by default), listed under `captions` in the catalogue.
- **One catalogue.** Every video on the site is declared once in `src/website/src/_data/videos.json`: encodings, size, length, upload date, and its label and caption in each language. Pages place a video with the `{% video "<id>" %}` shortcode (`src/website/video-figure.js`) and never repeat `<video>` markup. The shortcode renders in the page's language and emits a schema.org `VideoObject` whose name is the player's label, so search results describe exactly what a reader hears.
- **Placement.** A video goes wherever a page talks about what it shows: the home page (the profiler film autoplaying in the hero, the CPU recording in the profiler section), the Getting Started page (the tour), the profiler docs (the film and both recordings), and every blog post that discusses the profiler or the editor experience, in every language the page is published in.
- **Placement.** A video goes wherever a page talks about what it shows: the home page (the profiler film autoplaying in the hero, the CPU recording in the profiler section), the Getting Started page (the tour), the profiler docs (the film and both recordings), the F# profiling docs and the F# language page (the F# walkthrough), and every blog post that discusses the profiler or the editor experience, in every language the page is published in.

## [WEB-DESIGN-RESPONSIVE] Responsive Behavior

Expand Down
16 changes: 11 additions & 5 deletions docs/specs/DISTRIBUTION-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -867,10 +867,10 @@ inheriting a skip:

1. Any upstream job that FAILED or was CANCELLED fails `CI`, and with it the merge.
2. A SKIPPED upstream job satisfies it only when no code changed. That is how a
docs-only PR (every job skipped by [DIST-CI-CLASSIFICATION]) and a Dependabot
PR (the whole pipeline skipped, [DIST-CI-DEPENDABOT]) pass. When code changed,
`build`, every test leg and `coverage` MUST have succeeded; a skip among them
fails `CI` (#365).
docs-only PR (every job skipped by [DIST-CI-CLASSIFICATION]) passes. When code
changed, `build`, every test leg and `coverage` MUST have succeeded; a skip
among them fails `CI` (#365). A Dependabot PR, whose whole pipeline skipped
([DIST-CI-DEPENDABOT]), always fails `CI`: it ran nothing.
3. Every PHASE 4 leg and `coverage` states its condition explicitly:
`!cancelled()`, code changed, and success of the jobs it consumes. GitHub's
implicit `success()` spans the whole dependency chain, so a legitimately
Expand Down Expand Up @@ -929,7 +929,9 @@ superseded, and MUST NOT merge anything into `main` unattended.
staged batch is verified once, by the full pipeline and CodeQL, on the
`dependabot-upgrades → main` consolidation PR — the only route a dependency
change takes into `main`. A staged security fix reaches `main` when that PR
is opened, so it is opened promptly.
is opened, so it is opened promptly. `CI` enforces the route: it FAILS a
Dependabot PR to `main`, so a security bump whose sweep did not retire it
cannot merge untested.
4. `dependabot-automerge.yml` MUST exist on both branches: for `pull_request`,
GitHub runs the workflow from the PR's BASE branch.

Expand Down Expand Up @@ -1107,6 +1109,10 @@ Invariants:
distinction the manifest carries is `"linuxOnly": true` or
`"windowsOnly": true`, which drops a chunk from the other platform's matrix. A
chunk MUST NOT be declared in CI YAML.
- **A selection never runs nothing.** The in-host runner fails a run whose
`MOCHA_FILES` pattern matches no compiled suite, and, off Windows, one whose
pattern names only suites of `windowsOnly` chunks: dropping every one would
report a green run of zero tests (`test-runner-selection.test.ts`).
- **One runner, one shard target.** `make _run-vsix-suite` is the single recipe;
`CHUNK` selects the slice (empty runs every suite) and `VSIX_SUITE_PREBUILT`
says the suite is already compiled. Coverage is NOT a knob — the runner always
Expand Down
15 changes: 9 additions & 6 deletions docs/specs/DOTNET-FRAMEWORK-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ built at load or at the switch that needs them
- `sharplsp/targetFramework` `{ textDocument }` → `{ active, available, project }`;
`sharplsp/setTargetFramework` `{ textDocument, targetFramework }` switches the whole PROJECT in
both languages and answers the same shape. A single-target project answers
`{ active: null, available: [] }`; a document no loaded project compiles answers
`{ active: null, available: [], project: null }`, and switching it is an error.
`{ active: null, available: [] }`, even one whose name ends in `(tfm)`: a framework is
only the suffix `MSBuildWorkspace` appends to the project FILE's stem. A document no
loaded project compiles answers `{ active: null, available: [], project: null }`, and
switching it is an error.
- After a switch the host clears its navigation cache, re-publishes every open document's
diagnostics, sends `sharplsp/targetFrameworkChanged` `{ textDocument, active, available,
project }`, then each of `workspace/semanticTokens/refresh`, `workspace/inlayHint/refresh`,
`workspace/codeLens/refresh` and `workspace/diagnostic/refresh` whose `refreshSupport` the
client declared.
diagnostics and waits for each first publication (a failed or provisional fetch goes on
retrying in the background), sends `sharplsp/targetFrameworkChanged` `{ textDocument,
active, available, project }`, then each of `workspace/semanticTokens/refresh`,
`workspace/inlayHint/refresh`, `workspace/codeLens/refresh` and
`workspace/diagnostic/refresh` whose `refreshSupport` the client declared.
- VS Code: a status-bar item `$(versions) <tfm>` shows the focused document's active framework
when its project is multi-targeted, and runs `sharplsp.selectTargetFramework [tfm]`. Without
an argument the command opens a pick placeheld `Target framework for <Project>` (file name, no
Expand Down
7 changes: 4 additions & 3 deletions docs/specs/PROFILER-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ flowchart LR
- the record, the union case and the generic record each anchor, and are painted, at the exact `Program.fs` line that constructs them;
- the dashboard names the union case `HotLeakAppFs.PayloadKind+Small`;
- the diff scores the record as a leak;
- the leak's retention root is the module value `static items`, with no compiler-mangled name.
- the leak's retention root is the module value `static items`, with no compiler-mangled name;
- the fixture's line protocol builds the rest of the F# heap zoo one shape at a time: orders over a single-case union, a union case with fields, a list and a struct record; quotes behind a never-evicting cache; event-handler closures; a reference cycle; and arrays. Each anchors at its constructing line except arrays, which have none, and the growing record reaches Low confidence.
- File-system paths go only through `src/editors/vscode/src/paths.ts`.

### [PROFILER-SHARED-LIBRARY-ENGINE] Engine bundling
Expand Down Expand Up @@ -651,7 +652,7 @@ Commands used per node:

### [PROFILER-GRAPH-WEBVIEW] Interactive Graph Webview

The object graph renders as an interactive force-directed graph in a VS Code webview panel.
The object graph renders as a layered graph in a VS Code webview panel: GC roots on top, named by their labels, and the target objects along the bottom ([PROF-UI-REFGRAPH] in lspkit).

#### [PROFILER-GRAPH-WEBVIEW-LAYOUT] Graph Layout

Expand Down Expand Up @@ -762,7 +763,7 @@ output_directory = ".sharplsp/profiles"
| Command palette | Start/stop trace, start/stop counters, collect dump, leak detection |
| Webview panel | Display counter values as live-updating table |
| Webview panel | Display heap stats as sortable table |
| Webview panel | Interactive object retention graph (D3.js force-directed) |
| Webview panel | Object retention graph, layered from GC roots down to the targets |
| Webview panel | Heap snapshot diff table with growth indicators |
| Quick pick | Process selection from discovered .NET processes |
| File open | Open `.speedscope.json` output in browser/SpeedScope viewer |
Expand Down
Loading
Loading