diff --git a/.agents/skills/submit-pr/SKILL.md b/.agents/skills/submit-pr/SKILL.md index 700a404d..a21c83da 100644 --- a/.agents/skills/submit-pr/SKILL.md +++ b/.agents/skills/submit-pr/SKILL.md @@ -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 --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b45e17eb..0dd9df86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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() }} @@ -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." diff --git a/Makefile b/Makefile index 78588bf1..809dac3f 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 \ @@ -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= [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 diff --git a/docs/plans/PROFILER-PLAN.md b/docs/plans/PROFILER-PLAN.md index 5ea319a7..bb779b20 100644 --- a/docs/plans/PROFILER-PLAN.md +++ b/docs/plans/PROFILER-PLAN.md @@ -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`'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 diff --git a/docs/reviews/V0.22.0-STATIC-REVIEW.md b/docs/reviews/V0.22.0-STATIC-REVIEW.md index ce5e418e..840a40c1 100644 --- a/docs/reviews/V0.22.0-STATIC-REVIEW.md +++ b/docs/reviews/V0.22.0-STATIC-REVIEW.md @@ -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 diff --git a/docs/specs/DESIGN-SYSTEM.md b/docs/specs/DESIGN-SYSTEM.md index 16ee0826..37697c05 100644 --- a/docs/specs/DESIGN-SYSTEM.md +++ b/docs/specs/DESIGN-SYSTEM.md @@ -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/.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 (`..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 "" %}` shortcode (`src/website/video-figure.js`) and never repeat `