diff --git a/.agents/skills/ci-prep/SKILL.md b/.agents/skills/ci-prep/SKILL.md index dc4cb14..bf82979 100644 --- a/.agents/skills/ci-prep/SKILL.md +++ b/.agents/skills/ci-prep/SKILL.md @@ -7,113 +7,36 @@ argument-hint: "[--failing] [optional job name to focus on]" # CI Prep -Prepare the current state for CI. If CI is already failing, fetch and analyze the logs first. +Run what CI runs, locally, and fix it until green. -## Arguments +## If `--failing`: read the logs first -- `--failing` — Indicates a GitHub Actions run is already failing. When present, you MUST execute **Step 1** before doing anything else. -- Any other argument is treated as a job name to focus on (but all failures are still reported). +- Find the PR: `gh pr list --head "$(git branch --show-current)" --state open --json number --limit 1`. None → stop and say so. +- `gh pr checks `, then `gh run view --log-failed` for the latest run. +- Read every failed line. Note file, line and error. A job-name argument sets priority, but report everything. -If `--failing` is NOT passed, skip directly to **Step 2**. +## Read the workflow -## Step 1 — Fetch failed CI logs (only when `--failing`) +- Read `.github/workflows/ci.yml` end to end (plus `release.yml` for release work). Extract the commands it actually runs, in order. Never assume. +- Note env vars, matrices and conditionals. Flag any target beyond the standard `make` set. +- In `release.yml`, flag as blockers: checking out `main` instead of the tag SHA, any git commit/push/tag mutation, version-bump commits after tagging, `sed` version stamping. -You MUST do this before any other work. +## Run and fix -```bash -BRANCH=$(git branch --show-current) -PR_JSON=$(gh pr list --head "$BRANCH" --state open --json number,title,url --limit 1) -``` +- Run each CI command as CI does, in order. On failure: fix, re-run, then move on. +- Priority: `make fmt` → build → `make lint` → `make test`. +- Skip infra-only steps (checkout, toolchain setup, caching, uploads). -If the JSON array is empty, **stop immediately**: -> No open PR found for branch `$BRANCH`. Create a PR first. +## Never -Otherwise fetch the logs: +- Edit tests to pass, delete or skip tests, or remove assertions. +- Suppress lints (`#[allow]`, `oxlint-disable`, `#pragma warning disable`). +- Add `unsafe`, `unwrap`, `expect`, `panic!` or `todo!`. +- Lower `coverage-thresholds.json` or weaken any gate. +- Commit or push. -```bash -PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number') -gh pr checks "$PR_NUMBER" -RUN_ID=$(gh run list --branch "$BRANCH" --limit 1 --json databaseId --jq '.[0].databaseId') -gh run view "$RUN_ID" -gh run view "$RUN_ID" --log-failed -``` +## Report -Read **every line** of `--log-failed` output. For each failure note the exact file, line, and error message. If a job name argument was provided, prioritize that job but still report all failures. - -## Step 2 — Analyze the CI workflow - -1. Find the CI workflow file at `.github/workflows/ci.yml`. (Also look for `release.yml` if release work is involved.) -2. Read the workflow file completely. Parse every job and every step. -3. Extract the ordered list of commands the CI actually runs. In this repo CI runs (per REPO-STANDARDS-SPEC [MAKE-TARGETS]): `make fmt CHECK=1 → make lint → make test → make build`. Extract what is *actually there* — never assume. -4. Note any environment variables, matrix strategies, or conditional steps that affect execution. - -**Do NOT assume the steps are `make lint`, `make test`, `make build`.** Extract what the CI *actually does*. If you find extra targets beyond the 7 in [MAKE-TARGETS] (e.g. `make fmt-check`, `make coverage-check`), flag them in your final report — they should be consolidated by the agent-pmo skill. - -### Release workflow blocker scan - -If `.github/workflows/release.yml` exists, scan it before broad local CI. These are critical blockers -and must be fixed before release work is considered CI-ready: - -- Tag-triggered jobs checking out `ref: main` instead of the tagged SHA. -- Any `git commit`, `git push`, branch mutation, or tag mutation during release. -- Version bump commits after the tag already exists. -- Ad hoc `sed` version stamping of structured files instead of a first-class stamper/build input. -- Missing tests that pass a test version into the same stamper used by release. - -## Step 3 — Run each CI step locally, in order - -Work through failures in this priority order: - -1. **Formatting** — `make fmt` to clear noise; `make fmt CHECK=1` to verify -2. **Compilation errors** — `cargo build --workspace` must succeed before lint/test -3. **Lint violations** — `make lint` (`cargo clippy ... -D warnings`); fix the underlying code -4. **Runtime / test failures** — `make test` (fail-fast + coverage + threshold); fix source code to satisfy the test - -For each command extracted from the CI workflow: - -1. Run the command exactly as CI would run it (adjusting only for local environment differences like not needing `actions/checkout`). -2. If the step fails, **stop and fix the issues** before continuing to the next step. -3. After fixing, re-run the same step to confirm it passes. -4. Move to the next step only after the current one succeeds. - -### Hard constraints - -- **NEVER modify test files** — fix the source code, not the tests -- **NEVER add `#[allow(clippy::...)]` or `#[allow(...)]` suppressions** — fix the underlying code per CLAUDE.md -- **NEVER add `unsafe`** to bypass borrow-checker errors -- **NEVER use `unwrap()` / `expect()` / `panic!` / `todo!`** to silence type or error-handling issues -- **NEVER delete or ignore failing tests** -- **NEVER remove assertions** -- **NEVER lower the coverage threshold** in `coverage-thresholds.json` to make `_coverage_check` pass - -If stuck on the same failure after 5 attempts, ask the user for help. - -## Step 4 — Report - -- List every step that was run and its result (pass/fail/fixed). -- If any step could not be fixed, report what failed and why. -- Confirm whether the branch is ready to push. - -## Step 5 — Remote CI follow-up (only when `--failing`) - -Once all CI steps pass locally: - -1. Report the local fixes and exact commands that now pass. -2. Do not commit or push. The user owns source-control writes. -3. If the user pushes, monitor the new run until completion or failure. -4. Upon failure, go back to Step 1. - -## Rules - -- **Always read the CI workflow first.** Never assume what commands CI runs. -- Do not commit or push from this skill. -- Fix issues found in each step before moving to the next. -- Never skip steps or suppress errors. -- If the CI workflow has multiple jobs, run all of them (respecting dependency order). -- Skip steps that are CI-infrastructure-only (checkout, `dtolnay/rust-toolchain`, cache steps, artifact uploads) — focus on the actual build/test/lint commands. - -## Success criteria - -- Every command that CI runs has been executed locally and passed. -- All fixes are applied to the working tree. -- The CI passes successfully (if you are correcting an existing failure). +- Each step run and its result: pass, fail or fixed. +- Anything unfixed after 5 attempts, with the error and why. +- Whether the branch is ready to push. With `--failing`, list the exact commands that now pass; if the user pushes, watch the new run and loop back on failure. diff --git a/.agents/skills/code-dedup/SKILL.md b/.agents/skills/code-dedup/SKILL.md index 9f9a599..9bd38f1 100644 --- a/.agents/skills/code-dedup/SKILL.md +++ b/.agents/skills/code-dedup/SKILL.md @@ -6,102 +6,29 @@ description: Searches for duplicate code, duplicate tests, and dead code, then s # Code Dedup -Carefully search for duplicate code, duplicate tests, and dead code across the repo. Merge duplicates and delete dead code — but only when test coverage proves the change is safe. +Merge duplicates and delete dead code, only where tests prove it's safe. -## Prerequisites — hard gate +## Gate -Before touching ANY code, verify these conditions. If any fail, stop and report why. +- `make test` must be green (it's fail-fast and enforces `coverage-thresholds.json`). Red → stop. +- Record the coverage figure. That's the floor. +- Confirm workspace lints still deny `unsafe_code`, `missing_docs`, `unwrap_used`, `expect_used`, `panic`, `todo`. +- Untested files are off limits. -1. Run `make test` — all tests must pass. If tests fail, stop. Do not dedup a broken codebase. -2. `make test` is fail-fast AND enforces the coverage threshold from `coverage-thresholds.json`. If it exits non-zero, stop and fix it before deduping. -3. Rust is typed by default — proceed. (Workspace lints in `Cargo.toml` deny `unsafe_code`, `missing_docs`, and the clippy `unwrap_used` / `expect_used` / `panic` / `todo` / `unimplemented` / `unreachable` family — these guard rails are part of why dedup is safe here. Verify they are still active before starting.) +## Scan (list findings, change nothing yet) -## Steps +- **Duplicates:** Deslop `top-offenders`, then `find-similar` per hit (fused ≥ 0.85 is a candidate). Check across crates and packages; shared engine logic belongs behind `EngineApi` in `lspkit`. +- **Dead code:** `make lint` surfaces private dead code. For `pub` items, search every call site (tests, doc-tests, build scripts, configs). Dead only at zero references. +- **Duplicate tests:** the same assertions on the same path, or fixtures copied between crates. Keep the coarser E2E test. -Copy this checklist and track progress: +## Apply, one change at a time -``` -Dedup Progress: -- [ ] Step 1: Prerequisites passed (tests green, coverage met, workspace lints active) -- [ ] Step 2: Dead code scan complete -- [ ] Step 3: Duplicate code scan complete -- [ ] Step 4: Duplicate test scan complete -- [ ] Step 5: Changes applied -- [ ] Step 6: Verification passed (tests green, coverage stable) -``` - -### Step 1 — Inventory test coverage - -Before deciding what to touch, understand what is tested. - -1. Run `make test` to confirm green baseline. It exits non-zero on any test failure OR coverage shortfall (REPO-STANDARDS-SPEC [TEST-RULES], [COVERAGE-THRESHOLDS-JSON]). -2. Note the current line coverage percentage from `_coverage_check` output — this is the floor. It must not drop. -3. Identify which crates / modules have coverage and which do not. Only files WITH coverage are candidates for dedup. - -### Step 2 — Scan for dead code - -Search for code that is never called, never imported, never referenced. - -1. The Rust compiler already warns on dead code under the workspace lints (`dead_code = "deny"`, `unused_imports = "deny"`, `unused_variables = "deny"`). Run `make lint` and read every warning that survived denial — they will be hard errors. -2. For Rust public items (`pub fn`, `pub struct`, re-exports) the compiler will NOT flag them as dead. Run `cargo +nightly udeps` or grep the workspace for each `pub` item with zero call sites (including `tests/`, `examples/`, doc-tests, and downstream consumer references where known). -3. For each candidate: **grep the entire codebase** for references (including tests, doc tests in `///` comments, build scripts, configs). Only mark as dead if truly zero references. -4. List all dead code found with file paths and line numbers. Do NOT delete yet. - -### Step 3 — Scan for duplicate code - -Search for code blocks that do the same thing in multiple places. - -1. Look for functions/methods with identical or near-identical logic. -2. Look for copy-pasted blocks (same structure, maybe different variable names). -3. Look for multiple implementations of the same algorithm or pattern. -4. Check across crate boundaries — duplicates often hide between `lspkit-server`, `lspkit-mcp`, `lspkit-live`, `lspkit-vfs`, etc. The `EngineApi` trait surface in `lspkit` is the canonical place for shared engine logic. -5. For each duplicate pair: note both locations, what they do, and how they differ (if at all). -6. List all duplicates found. Do NOT merge yet. - -### Step 4 — Scan for duplicate tests - -Search for tests that verify the same behavior. - -1. Look for `#[test]` functions with identical assertions against the same code paths. -2. Look for test fixtures/helpers duplicated across `tests/` directories of different crates. -3. Look for integration tests that fully cover what a unit test (`mod tests { ... }`) also covers — keep the integration test per CLAUDE.md ("Coarse E2E only"). -4. List all duplicate tests found. Do NOT delete yet. - -### Step 5 — Apply changes (one at a time) - -For each change, follow this cycle: **change → test → verify coverage → continue or revert**. - -#### 5a. Remove dead code -- Delete dead code identified in Step 2. -- After each deletion: run `make test` (fail-fast + coverage + threshold all in one). -- If `make test` exits non-zero (test failure OR coverage drop): **revert immediately** and investigate. -- Dead code removal should never break tests or drop coverage. - -#### 5b. Merge duplicate code -- For each duplicate pair: extract the shared logic into a single function/module — usually the `lspkit` façade crate, but lower-level helpers belong with the crate that owns the domain (`lspkit-vfs` for buffer ops, etc.). -- Update all call sites to use the shared version. -- After each merge: run `make test`. -- If tests fail: **revert immediately**. The duplicates may have subtle differences you missed. -- If coverage drops: the shared code must have equivalent test coverage. Add tests if needed before proceeding. - -#### 5c. Remove duplicate tests -- Delete the redundant test (keep the more thorough one). -- After each deletion: run `make test`. -- If coverage drops below threshold, `make test` exits non-zero — **revert immediately**. The "duplicate" test was covering something the other wasn't. - -### Step 6 — Final verification - -1. Run `make lint` — clippy with `-D warnings` and the format check must pass. -2. Run `make test` — tests must pass AND coverage must remain ≥ the baseline from Step 1. -3. Report: what was removed, what was merged, final coverage vs baseline. - -(Only the 7 standard targets exist — `make lint` and `make test` cover formatting and coverage checks respectively.) +- Change → `make test` → coverage ≥ floor? Keep it. Otherwise revert immediately. +- Merged code goes to the crate that owns the domain. Update every call site. +- When deleting a duplicate test, keep the more thorough one. ## Rules -- **No test coverage = do not touch.** If a file has no tests covering it, leave it alone entirely. You cannot safely dedup what you cannot verify. -- **Coverage must not drop.** If removing or merging code causes coverage to decrease, revert and investigate. The coverage floor from Step 1 is sacred. -- **One change at a time.** Make one dedup change, run tests, verify coverage. Never batch multiple dedup changes before testing. -- **When in doubt, leave it.** If two code blocks look similar but you're not 100% sure they're functionally identical, leave both. False dedup is worse than duplication. -- **Preserve public API surface.** Do not change `pub` function signatures, struct names, trait method names, or re-exports that downstream consumers (lql-lsp-rust, Basilisk, Deslop, SharpLsp) depend on. Internal refactoring only. -- **Three similar lines is fine.** Do not create abstractions for trivial duplication. The cure must not be worse than the disease. Only dedup when the shared logic is substantial (>10 lines) or when there are 3+ copies. +- Not sure two blocks behave identically? Leave both. A false merge is worse than duplication. +- Don't change public API (`pub` signatures, names, re-exports). SharpLsp, Deslop, Basilisk and lql-lsp-rust depend on it. +- Finish with `make lint` and `make test`. Report what was removed and merged, and coverage against the floor. diff --git a/.agents/skills/fix-bug/SKILL.md b/.agents/skills/fix-bug/SKILL.md index 3eb8279..24336fe 100644 --- a/.agents/skills/fix-bug/SKILL.md +++ b/.agents/skills/fix-bug/SKILL.md @@ -6,62 +6,20 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash --- -# Bug Fix Skill — Test-First Workflow +# Fix Bug: test first -You MUST follow this exact workflow. Do NOT skip steps. Do NOT fix the bug before writing a failing test. +No fix until a test fails because of the bug. -## Step 1: Understand the Bug - -- Read the bug description: $ARGUMENTS -- Investigate the codebase to understand the relevant code -- Identify the root cause (or narrow down candidates) -- Summarize your understanding of the bug to the user before proceeding - -## Step 2: Write a Failing Test - -- Write a test that **directly exercises the buggy behavior** -- The test must assert the **correct/expected** behavior — so it FAILS against the current broken code -- The test name should clearly describe the bug (e.g., `test_orange_color_not_applied_to_head`) -- Use the project's existing test framework and conventions - -## Step 3: Run the Test — Confirm It FAILS - -- Run ONLY the new test (not the full suite) -- **Verify the test FAILS** and that it fails **because of the bug**, not for some other reason (typo, import error, wrong selector, etc.) -- If the test passes: your test does not capture the bug. Go back to Step 2 -- If the test fails for the wrong reason: fix the test, not the code. Go back to Step 2 -- **Repeat until the test fails specifically because of the bug** - -## Step 4: Show Failure to User - -- Show the user the test code and the failure output -- Explicitly ask: "This test fails because of the bug. Can you confirm this captures the issue before I fix it?" -- **STOP and WAIT for user acknowledgment before proceeding** -- Do NOT continue to Step 5 until the user confirms - -## Step 5: Fix the Bug - -- Make the **minimum change** needed to fix the bug -- Do not refactor, clean up, or "improve" surrounding code -- Do not change the test - -## Step 6: Run the Test — Confirm It PASSES - -- Run the new test again -- **Verify it PASSES** -- If it fails: go back to Step 5 and adjust the fix -- **Repeat until the test passes** - -## Step 7: Run the Full Test Suite - -- Run ALL tests to make sure nothing else broke -- If other tests fail: fix the regression without breaking the new test -- Report the final result to the user +- **Understand:** read the report ($ARGUMENTS) and the code, find the root cause, and state it in one line. +- **Write a failing test:** an E2E test that asserts the correct behavior, named after the bug, in the project's existing framework. +- **Prove it fails:** run only that test. It must fail *because of the bug*, not because of a typo or a bad import. If it passes or fails for the wrong reason, rewrite the test. +- **Show it:** report the test and the failure output, then keep going. Don't wait for confirmation. +- **Fix minimally:** the smallest change that fixes the bug. No refactors. Don't touch the test. +- **Prove it passes:** re-run the test, adjusting the fix until it's green. +- **Run everything:** `make test`. Fix regressions without weakening the new test. ## Rules -- NEVER fix the bug before the failing test is written and confirmed -- NEVER skip asking the user to acknowledge the test failure -- NEVER modify the test to make it pass — modify the source code -- If you cannot write a test for the bug, explain why and ask the user how to proceed -- Keep the fix minimal — one bug, one fix, one test +- Never edit the test to make it pass. +- One bug, one test, one fix. +- Can't write a test for it? Report why. Don't ship an untested fix. diff --git a/.agents/skills/spec-check/SKILL.md b/.agents/skills/spec-check/SKILL.md index 33722b9..e565ff0 100644 --- a/.agents/skills/spec-check/SKILL.md +++ b/.agents/skills/spec-check/SKILL.md @@ -5,266 +5,32 @@ argument-hint: "[optional spec ID or filename filter]" --- -# spec-check +# Spec Check -> **Portable skill.** This skill adapts to the current repository. The agent MUST inspect the repo structure and use judgment to apply these instructions appropriately. +Every spec section needs implementing code, tests, and logic that matches. Be pedantic and stop at the first failure. -Audit spec/plan documents against the codebase. Ensures every spec section has implementing code, tests, and that the code logic matches the spec. +`$ARGUMENTS` is optional: an exact ID (`PROF-WIRE-GEN`) checks that one section, and a path fragment (`lspkit`) checks those files. Empty means everything. No matches → error listing the spec files found. -## Arguments +## Find specs -- `$ARGUMENTS` — optional spec name or ID to check (e.g., `PARSE-CSHARP-NORMALIZE` or `lspkit`). If empty, check ALL specs. Spec IDs are descriptive slugs, NEVER numbered (see Step 1). +- Search `docs/**/*.md`, `SPEC.md`, `PLAN.md` and `specs/**/*.md` for IDs matching `\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\]`. +- For each ID, capture the heading text and the section body (up to the next heading of the same or higher level). -## Instructions +## Validate IDs first -Follow these steps exactly. Be strict and pedantic. Stop on the first failure. +- **Invalid:** numbered (`[SPEC-001]`, `[CI-004]`), trailing numbers (`[FEAT-AUTH-01]`), or no group (`[TIMEOUT]`). +- **Valid:** hierarchical slugs, `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]`, unique across the repo. +- **Clustered:** sections sharing a first word (the group) must be adjacent. +- **Complete:** headings that define behavior need an ID. +- Any violation → list them all with file:line and a suggested rename, then **stop**. ---- - -### Step 1: Validate spec ID structure - -Before checking code/test references, verify that the specs themselves are well-formed. - -1. Find all spec documents (see locations in Step 2). -2. Extract every section ID using the regex `\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\]`. -3. **Flag invalid IDs:** - - Numbered IDs (`[SPEC-001]`, `[REQ-003]`, `[CI-004]`) — must be renamed to descriptive hierarchical slugs. - - Single-word IDs (`[TIMEOUT]`) — must have a group prefix. - - IDs with trailing numbers (`[FEAT-AUTH-01]`) — the number is meaningless, remove it. -4. **Check group clustering:** The first word of each ID is its group. All sections in the same group MUST appear together (adjacent) in the document. If they're scattered, flag it. -5. **Check for missing IDs:** Any heading that defines a requirement or behavior should have an ID. Flag headings in spec files that look like they define behavior but lack an ID. - -If any ID violations are found, report them all and **STOP**: -``` -SPEC ID VIOLATIONS: - -- docs/specs/lspkit-spec.md line 12: [SPEC-001] → rename to descriptive ID (e.g., [PARSE-CSHARP-NORMALIZE]) -- docs/specs/lspkit-spec.md line 30: [PARSE-CSHARP-NORMALIZE] and [PARSE-CSHARP-LEX] are not adjacent (scattered group) -- docs/specs/lspkit-spec.md line 5: "## Coverage thresholds" has no spec ID - -Fix spec IDs first, then re-run spec-check. -``` - -If all IDs are valid, proceed to Step 2. - ---- - -### Step 2: Find all spec/plan documents - -Search for markdown files that contain spec sections with IDs. Look in these locations: - -- `docs/*.md` -- `docs/**/*.md` -- `SPEC.md` -- `PLAN.md` -- `specs/*.md` - -Use Glob to find candidate files, then use Grep to confirm they contain spec IDs. - -**Spec ID patterns** — IDs appear in square brackets, typically at the start of a heading or section line. Match this regex pattern: - -``` -\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\] -``` - -Spec IDs are **hierarchical descriptive slugs, NEVER numbered.** The format is `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]`. The first word is the **group** — all sections sharing the same group MUST appear together in the spec's table of contents. IDs are uppercase, hyphen-separated, unique across the repo, and MUST NOT contain sequential numbers. - -The hierarchy depth varies by repo: two words for simple repos (`[PARSE-LEX]`), three for most (`[PARSE-CSHARP-NORMALIZE]`), four for complex domains (`[PARSE-CSHARP-NORMALIZE-WHITESPACE]`). The hierarchy mirrors the spec document's heading structure. - -Examples of valid spec IDs (note how groups cluster): -- `[PARSE-CSHARP-NORMALIZE]`, `[PARSE-CSHARP-LEX]`, `[PARSE-CSHARP-COMMENTS]` — all in the PARSE-CSHARP group -- `[RANK-SCORE]`, `[RANK-WEIGHT]`, `[RANK-THRESHOLD]` — all in the RANK group -- `[VFS-EDIT]`, `[VFS-OPEN]` — all in the VFS group -- `[ENGINE-API]`, `[ENGINE-SNAPSHOT]` — all in the ENGINE group - -Examples of INVALID spec IDs: -- `[SPEC-001]` — numbered, meaningless -- `[FEAT-PARSE-01]` — trailing number -- `[REQ-003]` — sequential index, no group hierarchy -- `[CI-004]` — numbered, tells the reader nothing -- `[TIMEOUT]` — no group prefix, ungrouped - -For each file, extract every spec ID and its associated section title (the heading text after the ID) and the full section content (everything until the next heading of equal or higher level). - ---- - -### Step 3: Filter specs - -- If `$ARGUMENTS` is non-empty, filter the discovered specs: - - If it matches a spec ID exactly (e.g., `PARSE-CSHARP-NORMALIZE`), check only that spec. - - If it matches a partial name (e.g., `lspkit`), check all specs in files whose path contains that string. -- If `$ARGUMENTS` is empty, process ALL discovered specs. - -If filtering produces zero specs, report an error: -``` -ERROR: No specs found matching "$ARGUMENTS". Discovered spec files: [list them] -``` - ---- - -### Step 4: Check each spec section - -For EACH spec section that has an ID, perform checks A, B, and C below. **Stop on the first failure.** - -#### Check A: Code references the spec ID - -Search the entire codebase for the spec ID string, **excluding** these directories: -- `docs/` -- `target/` -- `.git/` -- `*.md` files (markdown is docs, not code) - -Use Grep with the literal spec ID (e.g., `[PARSE-CSHARP-NORMALIZE]`) to find references in code files. - -Rust code files should contain comments referencing the spec ID. The search must catch all Rust comment styles: - -**C-style `//` comments (Rust source):** -- `// Implements [PARSE-CSHARP-NORMALIZE]` -- `// [PARSE-CSHARP-NORMALIZE]` -- `// Tests [PARSE-CSHARP-NORMALIZE]` (also counts as a code reference) -- `/// Implements [PARSE-CSHARP-NORMALIZE]` (doc comments) -- `//! Implements [PARSE-CSHARP-NORMALIZE]` (inner doc comments) - -**Block comments:** -- `/* Implements [PARSE-CSHARP-NORMALIZE] */` - -**Hash `#` comments** (TOML, YAML, shell — for `Cargo.toml`, `*.yml`, `*.sh`): -- `# Implements [PARSE-CSHARP-NORMALIZE]` -- `# [PARSE-CSHARP-NORMALIZE]` - -**The key rule:** any comment containing the exact spec ID string (e.g., `[PARSE-CSHARP-NORMALIZE]`) counts as a valid code reference. The Grep search uses the literal spec ID string, so it naturally matches all comment styles. Do NOT restrict the search to specific comment prefixes — just search for the spec ID string itself. - -**If NO code files reference the spec ID:** - -``` -SPEC VIOLATION: [PARSE-CSHARP-NORMALIZE] "Section Title" has no implementing code. - -Every spec section must have at least one code file that references it via a comment -containing the spec ID (e.g., `// Implements [PARSE-CSHARP-NORMALIZE]`). - -ACTION REQUIRED: Add a comment referencing [PARSE-CSHARP-NORMALIZE] in the file(s) that implement -this spec section, then re-run spec-check. -``` - -**STOP HERE. Do not continue to other checks.** - -#### Check B: Tests reference the spec ID - -Search test files for the spec ID. Rust test files live in: -- `tests/` (integration tests) -- `**/tests/` -- `**/*_test.rs` -- Any `mod tests { ... }` block inside a regular `.rs` source file - -Use Grep to search these locations for the literal spec ID string. - -Tests should contain the spec ID in comments, test names, or annotations: - -**Rust:** -- `// Tests [PARSE-CSHARP-NORMALIZE]` -- `#[test] // Tests [PARSE-CSHARP-NORMALIZE]` -- `fn test_parse_csharp_normalize_trims_whitespace() { /* [PARSE-CSHARP-NORMALIZE] */ }` -- `#[test] fn parse_csharp_normalize_strips_comments() { ... }` (name itself contains the slug form) - -**The key rule:** same as Check A — search for the literal spec ID string in test files. Any occurrence of the exact spec ID in a test file counts. - -**If NO test files reference the spec ID:** - -``` -SPEC VIOLATION: [PARSE-CSHARP-NORMALIZE] "Section Title" has no tests. - -Every spec section must have corresponding tests that reference the spec ID. - -ACTION REQUIRED: Add tests for [PARSE-CSHARP-NORMALIZE] with a comment or test name containing -the spec ID, then re-run spec-check. -``` - -**STOP HERE. Do not continue to other checks.** - -#### Check C: Code logic matches the spec - -This is the most critical check. You must: - -1. **Read the spec section content carefully.** Understand exactly what behavior, logic, ordering, conditions, and constraints the spec describes. - -2. **Read the implementing code.** Use the references found in Check A to locate the implementing files. Read the relevant functions/sections. - -3. **Compare spec vs. code.** Be SENSITIVE and PEDANTIC. Check for: - - **Ordering violations** — If the spec says A happens before B, the code must do A before B. - - **Missing conditions** — If the spec says "only when X", the code must have that condition. - - **Extra behavior** — If the code does something the spec doesn't mention, flag it only if it contradicts the spec. - - **Wrong logic** — If the spec says "greater than" but code uses "greater than or equal", that's a violation. - - **Missing steps** — If the spec describes 5 steps but code only implements 3, that's a violation. - - **Wrong defaults** — If the spec says "default to X" but code defaults to Y, that's a violation. - -4. **If the code deviates from the spec**, report a detailed error: - -``` -SPEC VIOLATION: [PARSE-CSHARP-NORMALIZE] Code does not match spec. - -SPEC SAYS: -> "Normalization must strip leading/trailing whitespace BEFORE removing comment lines" -> (from docs/specs/lspkit-spec.md, line 42) - -CODE DOES: -> `let s = strip_comments(input); let s = s.trim();` (crates/lspkit-treesitter/src/normalize.rs:42) - -DEVIATION: The code strips comments BEFORE trimming whitespace. -The spec explicitly requires trimming FIRST. - -ACTION REQUIRED: Reorder the logic in crates/lspkit-treesitter/src/normalize.rs to trim -whitespace before stripping comments, as specified in [PARSE-CSHARP-NORMALIZE]. -``` - -**STOP HERE. Do not continue to other specs.** - -5. **If the code matches the spec**, this check passes. Move to the next spec. - ---- - -### Step 5: Report results - -#### On failure (any check fails): - -Output ONLY the first violation found. Use the exact error format shown above. Do not summarize other specs. Do not offer to fix the code. Just report the violation. - -End with: -``` -spec-check FAILED. Fix the violation above and re-run. -``` - -#### On success (all specs pass): - -Output a summary table: - -``` -spec-check PASSED. All specs verified. - -| Spec ID | Title | Code References | Test References | Logic Match | -|-----------------------------|--------------------------|-------------------------------|----------------------------------|-------------| -| [PARSE-CSHARP-NORMALIZE] | C# input normalization | crates/lspkit-treesitter/src/normalize.rs | crates/lspkit-treesitter/tests/normalize.rs | PASS | -| [ENGINE-API] | EngineApi trait surface | crates/lspkit/src/engine.rs | crates/lspkit/tests/engine.rs | PASS | -| ... | ... | ... | ... | ... | - -Checked N spec sections across M files. All have implementing code, tests, and matching logic. -``` - ---- - -## Search strategy summary +## Check each section, stopping at the first failure -1. **Validate spec IDs:** Check all IDs are hierarchical, descriptive, grouped, and non-numbered -2. **Find spec files:** Glob for `docs/**/*.md`, `SPEC.md`, `PLAN.md`, `specs/**/*.md` -3. **Extract spec IDs:** Grep for `\[[A-Z][A-Z0-9]*(-[A-Z0-9]+)+\]` in those files -4. **Find code refs:** Grep for the literal spec ID in all files, excluding `docs/`, `target/`, `.git/`, `*.md` -5. **Find test refs:** Grep for the literal spec ID in test directories and test file patterns -6. **Read and compare:** Read the spec section content and the implementing code, compare logic +- **Code refs:** grep the literal ID outside `docs/`, `target/`, `.git/` and `*.md`. Any comment style counts. None → violation. +- **Test refs:** grep the literal ID in test code (`tests/`, `*_test.rs`, `mod tests`, `packages/*/test/`, `src/test/`, `dotnet/*.Tests/`). None → violation. +- **Logic match:** read the section, then the code it cites. Flag wrong order, missing conditions, wrong comparisons, missing steps, wrong defaults, and extra behavior that contradicts the spec. -## Key principles +## Report -- **Fail fast.** Stop on the first violation. One fix at a time. -- **Be pedantic.** If the spec says it, the code must do it. No "close enough". -- **Quote everything.** Always quote the spec text and the code in error messages so the developer sees exactly what's wrong. -- **Be actionable.** Every error must tell the developer what file to change and what to do. -- **Exclude docs from code search.** Markdown files are documentation, not implementation. Only search actual code files for spec references. -- **No numbered IDs.** Spec IDs are hierarchical descriptive slugs (`[PARSE-CSHARP-NORMALIZE]`), NEVER sequential numbers (`[SPEC-001]`). The first word is the group — sections sharing a group must be adjacent in the TOC. If you encounter numbered or ungrouped IDs, flag them as a violation. +- **On failure:** only the first violation, quoting the spec text with file:line, quoting the code with file:line, stating the deviation and the exact fix. End with `spec-check FAILED. Fix the violation above and re-run.` Don't fix anything. +- **On success:** `spec-check PASSED.` plus a table of ID | Title | Code refs | Test refs | Logic, and a count of sections and files. diff --git a/.agents/skills/submit-pr/SKILL.md b/.agents/skills/submit-pr/SKILL.md index dd9b192..2895802 100644 --- a/.agents/skills/submit-pr/SKILL.md +++ b/.agents/skills/submit-pr/SKILL.md @@ -7,33 +7,33 @@ disable-model-invocation: false # Submit PR -Create a pull request for the current branch with a well-structured description. - -## Steps - -*NOTE: if you already ran make ci in this session and it passed, you can skip step 1.* - -1. Run `make ci` — must pass completely before creating PR -2. **Generate the diff against main.** Run `git diff main...HEAD > /tmp/pr-diff.txt` to capture the full diff between the current branch and the head of main. This is the ONLY source of truth for what the PR contains. **Warning:** the diff can be very large. If the diff file exceeds context limits, process it in chunks (e.g., read sections with `head`/`tail` or split by file) rather than trying to load it all at once. -3. **Derive the PR title and description SOLELY from the diff.** Read the diff output and summarize what changed. Ignore commit messages, branch names, and any other metadata — only the actual code/content diff matters. -4. Write PR body using the template in `.github/pull_request_template.md` -5. Fill in (based on the diff analysis from step 3): - - TLDR: one sentence - - What Was Added: new files, features, deps - - What Was Changed/Deleted: modified behaviour - - How Tests Prove It Works: specific test names or output - - Spec/Doc Changes: if any - - Breaking Changes: yes/no + description -6. Use `gh pr create` with the filled template +- **Gate:** `make ci` must pass. Skip if it already passed this session. +- **Diff:** `git diff main...HEAD` into the scratchpad. It's the only source of truth. Read big diffs in chunks. +- **Write from the diff only.** Ignore commit messages and branch names. +- **Body:** fill `.github/pull_request_template.md`: + - TLDR (one sentence) + - Added + - Changed/Deleted + - How tests prove it (name the tests) + - Spec/doc changes + - Breaking changes +- **Create:** `gh pr create`, linking the issue if one exists. Return the URL. + +## Loop until green + +The job isn't done when the PR opens. It's done when every check is green. + +- **Watch:** `gh pr checks --watch` until every check finishes. +- **Green:** report the URL and stop. +- **Red:** + - Read every failed line: `gh run view --log-failed`. + - Fix the code, following [ci-prep](../ci-prep/SKILL.md). + - Re-run the failing step locally until it passes. + - Commit and push, then go back to **Watch**. +- **Stuck:** after 5 rounds on the same failure, report the error and what you tried. ## Rules -- Never create a PR if `make ci` fails -- PR description must be specific and tight — no vague placeholders -- Link to the relevant GitHub issue if one exists - -## Success criteria - -- `make ci` passed -- PR created with `gh pr create` -- PR URL returned to user +- No PR if `make ci` fails. +- Specific and tight. No vague placeholders. +- Never weaken a gate, skip a test or suppress a lint to turn a check green. diff --git a/.agents/skills/upgrade-packages/SKILL.md b/.agents/skills/upgrade-packages/SKILL.md index 6b6651a..bf49b93 100644 --- a/.agents/skills/upgrade-packages/SKILL.md +++ b/.agents/skills/upgrade-packages/SKILL.md @@ -7,92 +7,42 @@ argument-hint: "[--check-only] [--major] [crate-name]" # Upgrade Packages -Upgrade Cargo dependencies to their latest compatible (or latest major, if `--major`) versions. +`--check-only` lists what's outdated and stops. `--major` allows breaking bumps. Any other argument names a single package. -## Arguments +## List first -- `--check-only` — List outdated crates without upgrading. Stop after Step 2. -- `--major` — Include major version bumps (breaking changes). Without this flag, stay within semver-compatible ranges. -- Any other argument is treated as a specific crate name to upgrade (instead of all crates). +- **Cargo:** `cargo outdated --workspace`, `cargo update --dry-run`. Versions live once, in the root `[workspace.dependencies]`. +- **npm:** `npm outdated` in each `packages/*`. TypeScript stays on `^7`. +- **.NET:** `dotnet list dotnet/ package --outdated`. +- With `--check-only`, report and stop here. -## Step 1 — Detect manifests +## Read the docs -This is a Rust workspace. Manifests to consider: +- Fetch the [cargo update docs](https://doc.rust-lang.org/cargo/commands/cargo-update.html) before running anything. Don't guess flags. -| Manifest file | Purpose | -|---|---| -| `Cargo.toml` (workspace root) | `[workspace.dependencies]` table — single source of truth for versions | -| `crates/*/Cargo.toml` | Per-crate dependency tables (should re-export from `[workspace.dependencies]`) | +## Upgrade -If `Cargo.toml` is missing, stop and tell the user. +- **Cargo:** `cargo update`. With `--major`, `cargo update --breaking`. For one crate: `cargo update -p [--precise ]`. +- **npm:** `npm install @latest` per package. +- **Versions:** keep them in sync across manifests, `.github/workflows/` and `.devcontainer/`. -## Step 2 — List outdated crates +## Verify -Run before upgrading anything. Show the user what will change. +- `make ci`. On failure, read the changelog (tower-lsp, rmcp, tokio, lsp-types and tree-sitter break often) and fix the source. +- Stuck after 3 attempts on one failure: report the package and the error. -```bash -cargo outdated --workspace # install: cargo install cargo-outdated -cargo update --dry-run -``` +## Never -**Read the docs:** https://doc.rust-lang.org/cargo/commands/cargo-update.html +- Edit lockfiles by hand. +- Downgrade, unless rolling back a broken upgrade. +- Remove packages, unless they're deprecated and replaced. +- Suppress post-upgrade warnings. +- Commit. -If `--check-only` was passed, **stop here** and report the outdated list. +## Report -## Step 3 — Read the official upgrade docs - -**Before running any upgrade command, you MUST fetch and read the official documentation URL above.** Use WebFetch. Do not guess at flags from memory. Pay particular attention to `cargo update --breaking` (cargo 1.84+) — that is the flag for cross-major upgrades. - -## Step 4 — Upgrade crates - -```bash -cargo update # semver-compatible updates -# --major flag: -cargo update --breaking # major version bumps (cargo 1.84+) -``` - -For a specific crate: `cargo update -p ` (or `cargo update -p --precise ` for pinning). - -Workspace dependency versions are centralised in `[workspace.dependencies]` at the root `Cargo.toml`. For breaking version bumps that require manifest edits (e.g. `tokio 1 -> 2`), update the version string in `[workspace.dependencies]` once — per-crate `Cargo.toml` files that use `tokio = { workspace = true }` will pick it up automatically. - -## Step 5 — Verify the upgrade - -After upgrading, run the full CI pipeline locally: - -```bash -make ci -``` - -If `make ci` fails: -1. Read the failure output carefully. -2. Check the changelog / release notes for the upgraded crates. For tower-lsp, rmcp, tokio, lsp-types, and tree-sitter, breaking changes are common across majors — fetch the relevant release notes URL. -3. Fix breaking changes in source code (NOT in tests, NOT via `#[allow]` suppressions). -4. Re-run `make ci`. -5. If stuck after 3 attempts on the same failure, report it to the user with the error details and the crate that caused it. - -## Step 6 — Report - -Provide a summary: - -- Crates upgraded (old version -> new version) -- Crates skipped (and why, e.g., major version bump without `--major` flag) -- `make ci` result after upgrade -- Any breaking changes that were fixed (with file references) -- Any crates that could not be upgraded (with error details) - -## Rules - -- **Always list outdated crates first** before upgrading anything. -- **Always read the official `cargo update` docs** before running upgrade commands. -- **Always run `make ci` after upgrading** to catch breakage immediately. -- **Never remove crates** unless they were explicitly deprecated and replaced (e.g., a future migration from `tower-lsp` to `async-lsp`). -- **Never downgrade crates** unless rolling back a broken upgrade. -- **Never modify `Cargo.lock` manually** — let cargo regenerate it. -- **Never add `#[allow(...)]`** to silence post-upgrade warnings — fix the underlying code. -- **Commit nothing** — leave changes in the working tree for the user to review. - -## Success criteria - -- All outdated crates upgraded to latest compatible (or latest major if `--major`). -- `make ci` passes (lint + test + build, with coverage threshold). -- User has a clear summary of what changed. +- old → new for each upgraded package. +- Skipped packages, and why. +- The `make ci` result. +- Breaking changes fixed, with file references. +- Failures. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a54bb0f..1fe9eeb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,20 +1,76 @@ version: 2 -# Keeps SHA-pinned actions and Cargo dependencies fresh. Each ecosystem is -# grouped so a run opens a single combined PR, not one per package. -# [SWR-SEC-ACTION-PINNING] +# Keeps SHA-pinned actions and every dependency graph fresh without burning CI +# on `main`. EVERY bump lands on the `dependabot-upgrades` staging branch: +# version PRs target it directly, and dependabot-automerge.yml sweeps security +# PRs (which GitHub always opens against `main`) into it too. ci.yml and +# codeql.yml skip Dependabot PRs, so the batch reaches `main` through ONE +# consolidation PR where CI runs once. Each language ecosystem collapses all +# version bumps into one group and all security bumps into a `*-security` +# group. A 7-day cooldown keeps a freshly published (possibly hijacked) +# release out until the ecosystem has had a week to flag it. +# [REL-DEPENDABOT] [GITHUB-DEPENDABOT] [SWR-SEC-ACTION-PINNING] updates: - package-ecosystem: github-actions - directory: "/" + directory: / + target-branch: "dependabot-upgrades" schedule: interval: weekly + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: ["dependencies"] groups: github-actions: patterns: ["*"] - package-ecosystem: cargo - directory: "/" + directory: / + target-branch: "dependabot-upgrades" schedule: interval: weekly + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: ["dependencies"] groups: cargo: + applies-to: version-updates + patterns: ["*"] + cargo-security: + applies-to: security-updates + patterns: ["*"] + + - package-ecosystem: npm + directories: + - /packages/lsp-profiler-ui + target-branch: "dependabot-upgrades" + schedule: + interval: weekly + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: ["dependencies"] + groups: + npm: + applies-to: version-updates + patterns: ["*"] + npm-security: + applies-to: security-updates + patterns: ["*"] + + - package-ecosystem: nuget + directory: /dotnet + target-branch: "dependabot-upgrades" + schedule: + interval: weekly + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + labels: ["dependencies"] + groups: + nuget: + applies-to: version-updates + patterns: ["*"] + nuget-security: + applies-to: security-updates patterns: ["*"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7fc486..a20a483 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,12 @@ # agent-pmo:74cf183 name: CI +# Release-only regime: CI gates PRs to `main` and nothing else. A merge to +# `main` triggers nothing (every commit on it arrived through a green PR), and +# shipping happens only when release.yml fires on a `v*` tag. [REL-REGIME] [CI-WORKFLOWS] on: pull_request: branches: [main] - push: - branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -19,6 +20,9 @@ permissions: jobs: ci: name: CI + # Dependabot PRs are swept into `dependabot-upgrades` and reach `main` only + # through one consolidation PR, where CI runs once. [GITHUB-DEPENDABOT] + if: github.actor != 'dependabot[bot]' # Public repositories can use standard runners without larger-runner charges. runs-on: ubuntu-22.04 timeout-minutes: 15 @@ -53,7 +57,7 @@ jobs: # rustc-version-aware key, and target/ cleanup before save (avoids bloat). # Survives Cargo.toml dev-dep changes via partial restore, unlike the # previous hashFiles(**/Cargo.toml) key. - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 with: cache-on-failure: true # Cache the workspace across all steps in this job; key already @@ -63,7 +67,7 @@ jobs: # Prebuilt binaries via taiki-e/install-action — seconds vs. minutes # compared to `cargo install --locked` building from source. - name: Install cargo-llvm-cov + cargo-nextest - uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29 + uses: taiki-e/install-action@5488ac5a951645ce95062214902ac4b6bc1a1037 # v2.87.32 with: tool: cargo-llvm-cov,cargo-nextest diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..19fd71c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,152 @@ +name: CodeQL + +# CodeQL static security analysis ([GITHUB-CODE-SCANNING]). +# +# SEPARATE from ci.yml on purpose: CodeQL feeds GitHub code-scanning alerts and +# needs `security-events: write` + a weekly schedule, while ci.yml owns +# lint/test/build. It does NOT overlap with `make lint` (style/correctness) or +# dependency-review (vulnerable packages) — CodeQL finds vulnerable CODE. Never +# add security-rule linter plugins that re-cover CodeQL: no doubling up. +# +# Matrix = lspkit's languages that CodeQL supports (Rust, TypeScript, C#) plus +# `actions`, which scans the workflow files themselves. F# has no CodeQL +# support. Action SHAs are kept current by the github-actions Dependabot group +# ([GITHUB-DEPENDABOT]). +on: + pull_request: + branches: [main] + schedule: + # Weekly, so newly-published CodeQL queries re-scan even without a push. + - cron: "27 4 * * 1" + # release.yml calls this with gate=true on every v* tag to scan the exact + # released SHA with the current query set and BLOCK publishing on any + # High/Critical finding. The PR scan covers the diff, the weekly scan covers + # query drift, the gated call covers the released commit itself — as a HARD + # gate, not advice: a finding FAILS the release. This replaces the old + # standalone `push: [tags]` scan, which could only file alerts AFTER the + # artifact had already shipped — useless as a gate. [GITHUB-CODE-SCANNING] + workflow_call: + inputs: + gate: + description: >- + When true (release calls), fail the job on any High/Critical finding so + the calling release workflow cannot publish. PR/weekly runs leave this + false and stay advisory (the PR check-failure threshold governs merges). + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + # Code scanning (SARIF upload) requires GitHub Advanced Security on PRIVATE + # repos. Gating on public visibility lets a private repo skip cleanly (no red + # X) and self-enable the moment it is made public — no follow-up edit needed. + # Dependabot PRs are excluded: they are swept into `dependabot-upgrades` by + # dependabot-automerge.yml and never merge to main directly, so scanning them + # only burns the matrix on a bump we discard — CodeQL runs on the + # consolidation PR instead. ([GITHUB-DEPENDABOT]) + if: github.event.repository.visibility == 'public' && github.actor != 'dependabot[bot]' + permissions: + security-events: write + actions: read + contents: read + strategy: + fail-fast: false + matrix: + # `build-mode: none` analyses Rust, TypeScript and C# without + # re-compiling what ci.yml already builds. + include: + - language: actions # scans the workflow files themselves + build-mode: none + - language: rust + build-mode: none + - language: javascript-typescript + build-mode: none + - language: csharp + build-mode: none + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 + with: + category: "/language:${{ matrix.language }}" + # Drop SARIF on disk so the gate step can read it. `upload` stays on + # (default) so alerts still post to code scanning on every run. + output: sarif-results + # Release gate. `security-severity` is the 0-10 CVSS-style score CodeQL + # attaches to each security rule; >= 7.0 == High or Critical. Enforced ONLY + # on gated (release) calls — PR/weekly runs skip this and stay advisory. + # Caveat: this reads freshly produced SARIF, which does NOT reflect alert + # dismissals — a dismissed false positive re-blocks until excluded via a + # CodeQL config. FAILS CLOSED: missing/malformed SARIF errors, never passes. + # [GITHUB-CODE-SCANNING] + - name: Enforce no high/critical findings (release gate) + if: inputs.gate + shell: bash + env: + SARIF_DIR: sarif-results + SEVERITY_THRESHOLD: '7.0' + run: |- + set -euo pipefail + shopt -s nullglob + # Fail closed: no SARIF means we cannot prove the code is clean. + sarifs=( "${SARIF_DIR}"/*.sarif ) + if [ "${#sarifs[@]}" -eq 0 ]; then + echo "::error::CodeQL gate: no SARIF in ${SARIF_DIR}; cannot verify findings — failing closed." + exit 1 + fi + offenders=0 + for sarif in "${sarifs[@]}"; do + if ! jq -e '.runs' "${sarif}" >/dev/null 2>&1; then + echo "::error::CodeQL gate: ${sarif} is not valid SARIF (no .runs) — failing closed." + exit 1 + fi + # Observability: a clean scan logs results=0 with a non-zero + # severity_rules count, proving real SARIF was parsed. + jq -r --arg f "${sarif##*/}" ' + ([ (.runs[].tool.driver.rules // [])[], + (.runs[].tool.extensions[]?.rules // [])[] ]) as $rules + | "CodeQL gate: \($f): results=\([.runs[].results[]?]|length) severity_rules=\([$rules[]|select(.properties["security-severity"])]|length)" + ' "${sarif}" + # CodeQL puts query rules in tool.extensions[].rules (driver.rules is + # empty in CodeQL output); union both, then keep results >= threshold. + hits="$(jq -r --argjson t "${SEVERITY_THRESHOLD}" ' + .runs[] + | ( [ (.tool.driver.rules // [])[], + (.tool.extensions[]?.rules // [])[] ] + | map({ key: .id, + value: ((.properties["security-severity"] // "0") | tonumber) }) + | from_entries + ) as $severity + | .results[] + | select( ($severity[.ruleId] // 0) >= $t ) + | .ruleId + ' "${sarif}" | sort | uniq -c | sort -rn)" + if [ -n "${hits}" ]; then + echo "::error::High/critical CodeQL findings in ${sarif}:" + echo "${hits}" + offenders=$((offenders + 1)) + fi + done + if [ "${offenders}" -gt 0 ]; then + echo "::error::CodeQL gate failed — release blocked. Fix or dismiss-and-exclude the findings, then re-tag." + exit 1 + fi + echo "CodeQL gate passed: nothing at or above severity ${SEVERITY_THRESHOLD}." diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..2c5a72c --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,91 @@ +name: Dependabot auto-merge + +# Sweeps EVERY Dependabot PR into the long-lived `dependabot-upgrades` staging +# branch, no questions asked ([GITHUB-DEPENDABOT]). Two kinds of PR land here: +# +# * VERSION updates -> Dependabot opens them against `dependabot-upgrades` +# directly (.github/dependabot.yml `target-branch`). +# * SECURITY updates -> GitHub IGNORES `target-branch` for these and ALWAYS +# opens them against the default branch (`main`). So this workflow also +# triggers on `main` and folds the security bump into the SAME staging +# branch — nothing is ever left sitting on `main` waiting for a human. +# +# Merge strategy: the incoming branch ALWAYS clobbers what is already staged +# (`git merge -X theirs`). Successive bumps of the same lock-file never conflict- +# stall: the latest bump wins, every time. Nothing reaches `main` this way — the +# full build/test (ci.yml) + CodeQL (codeql.yml) gate the single +# `dependabot-upgrades -> main` consolidation PR, which is where review and the +# expensive matrix actually run. ci.yml/codeql.yml deliberately SKIP Dependabot +# PRs (they would only burn the matrix on a bump we immediately sweep away). +# +# Lives at the repo root so it is present on `dependabot-upgrades` (cut from +# main): for `pull_request` the workflow is read from the PR's base branch, so +# BOTH `main` and the staging branch must carry this file. +on: + pull_request: + branches: + - dependabot-upgrades + - main + +# Least-privilege default; the sweep job elevates only what it needs. +# [SWR-SEC-TOKEN-PRIVILEGE] +permissions: + contents: read + +jobs: + sweep: + name: Clobber-merge into dependabot-upgrades + # The PR author, not `github.actor`: the actor is whoever triggered this + # run (anyone re-running it), so it is spoofable. + if: github.event.pull_request.user.login == 'dependabot[bot]' + # Deliberately the standard runner, NOT a larger/paid one: a trivial merge + # bot must not consume CI minutes meant for the real build matrix. + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write # push the bump onto dependabot-upgrades + pull-requests: write # close the swept PR + steps: + - name: Check out the staging branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: dependabot-upgrades + fetch-depth: 0 + persist-credentials: false + + - name: Clobber-merge the bump and retire the PR + env: + PR_URL: ${{ github.event.pull_request.html_url }} + PR_HEAD: ${{ github.event.pull_request.head.ref }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + # Push through gh's credential helper; the token never lands in .git/config. + gh auth setup-git + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Pull the bump branch into a stable local ref we can re-merge. + git fetch origin "+refs/heads/${PR_HEAD}:refs/remotes/origin/${PR_HEAD}" + # Re-merge onto the LIVE staging tip and retry: concurrent Dependabot + # PRs race to push here, so each run rebases on whatever already landed + # and the incoming branch always wins conflicts (-X theirs). + for attempt in 1 2 3 4 5; do + git fetch origin "+refs/heads/dependabot-upgrades:refs/remotes/origin/dependabot-upgrades" + git reset --hard "origin/dependabot-upgrades" + git merge -X theirs --no-edit "origin/${PR_HEAD}" \ + -m "build(deps): clobber-merge ${PR_HEAD} into dependabot-upgrades" + if git push origin "HEAD:dependabot-upgrades"; then + break + fi + if [ "$attempt" = "5" ]; then + echo "::error::could not push to dependabot-upgrades after 5 attempts" + exit 1 + fi + sleep 5 + done + # Retire the PR + its branch: the bump is already staged, so the PR + # (whether it targeted main or the staging branch) has served its + # purpose. `|| true` — GitHub may have auto-closed it on the push. + gh pr close "$PR_URL" --delete-branch \ + --comment "Swept into \`dependabot-upgrades\` (latest bump clobbers previous)." \ + || git push origin --delete "$PR_HEAD" || true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 574f98a..364bbfc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,31 +1,58 @@ name: release -# Tag-triggered crates.io publish. A library workspace has no binaries, so the -# "release" is the set of crates published to crates.io — no GitHub Release -# archives, Homebrew, Scoop, or VSIX apply here. [SWR-REL-WORKFLOW] +# The ONLY path to ship. Pushing a `v*` tag runs the full CI gate on the tagged +# SHA, cuts a signed GitHub Release, then publishes every crate to crates.io +# and every package under packages/ to npm. Nothing ships on a merge to `main` +# or on a schedule. [REL-REGIME] [REL-TRIGGER] [CI-WORKFLOWS] [SWR-REL-WORKFLOW] +# +# lspkit is a library workspace with no executables, so there are no binary +# archives, Homebrew, Scoop or VSIX channels, and no costly platform matrix for +# release change detection to gate. [SWR-REL-CHANGES-CONTRACT] on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - "v[0-9]+.[0-9]+.[0-9]+-*" -# Least-privilege default; the publish job elevates only what it needs. +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +# Least-privilege default; each job elevates only what it needs. # [SWR-SEC-TOKEN-PRIVILEGE] permissions: contents: read jobs: - publish: - name: Publish to crates.io + # Hard gate: scan the exact tagged SHA and block publishing on any High or + # Critical finding. [REL-GATES] [GITHUB-CODE-SCANNING] + codeql: + name: CodeQL gate + uses: ./.github/workflows/codeql.yml + with: + gate: true + permissions: + security-events: write + actions: read + contents: read + + release: + name: Release + needs: codeql runs-on: ubuntu-latest - timeout-minutes: 30 - # Protected environment gates the OIDC publish (reviewer + tag restriction). + # TIMEOUT EXCEPTION: one job stamps, runs the full `make ci` gate, packages, + # signs, cuts the GitHub Release and publishes 4 crates + 1 npm package. + timeout-minutes: 45 + # Protected environment gates the OIDC publishes (reviewer + tag restriction). environment: release permissions: - contents: read - id-token: write # OIDC -> crates.io Trusted Publishing. [SWR-SEC-OIDC-PUBLISH] + contents: write # create the GitHub Release + id-token: write # OIDC: crates.io + npm Trusted Publishing, cosign keyless. [SWR-SEC-OIDC-PUBLISH] + attestations: write # build provenance + SBOM attestations. [SWR-SEC-PROVENANCE] + env: + DIST: dist steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -40,37 +67,49 @@ jobs: with: dotnet-version: "10.0.x" - # crates/lsp-profiler/build.rs generates the wire model from - # docs/models/profiler.td with typeDiagram. [PROF-WIRE-GEN] + # registry-url tells npm which registry to mint an OIDC publish token for. + # No caches anywhere in this job: a poisoned cache entry must never reach + # a published artifact. - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: "22" + registry-url: "https://registry.npmjs.org" + package-manager-cache: false - - name: Install typeDiagram - run: npm install -g typediagram@0.11.0 - - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + # npm Trusted Publishing needs npm >= 11.5.1; Node 22 bundles an older one. + # crates/lsp-profiler/build.rs generates the wire model with typeDiagram. + # [SWR-SEC-OIDC-PUBLISH] [PROF-WIRE-GEN] + - name: Install npm + typeDiagram + run: npm install -g npm@11.21.1 typediagram@0.11.0 - name: Install release tooling (coverage, test runner, version stamper, vuln gate) - uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29 + uses: taiki-e/install-action@5488ac5a951645ce95062214902ac4b6bc1a1037 # v2.87.32 with: tool: cargo-llvm-cov,cargo-nextest,shipwright-version-stamp,cargo-deny - # Fail fast with a precise error if the single license source-of-truth is - # absent on the runner. It MUST be committed (git-tracked) at the repo root; - # only the per-crate staged copies (crates/*/LICENSE-MIT) are gitignored. - # Without this guard the staging step below would abort with a cryptic - # `cp: No such file or directory`. [SWR-REL-LICENSE] + # `make test` traces the profiled fixture's syscalls, as in ci.yml. + - name: Install syscall tracer + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends strace + + # The single license source-of-truth MUST be committed at the repo root; + # only the staged per-package copies are gitignored. [SWR-REL-LICENSE] - name: Verify license source-of-truth is present run: | test -f LICENSE-MIT || { - echo "::error::LICENSE-MIT missing from checkout. Commit it (git-tracked) at the repo root — it is staged into every crate at publish time. [SWR-REL-LICENSE]" + echo "::error::LICENSE-MIT missing from checkout. Commit it (git-tracked) at the repo root — it is staged into every crate and npm package at publish time. [SWR-REL-LICENSE]" exit 1 } - # Stamp the tag into every Cargo.toml version carrier in the runner working - # tree only. The tagged commit keeps `0.0.0-dev`; nothing is committed back. - # [SWR-VERSION-BUILD-STAMPING] + # Install from the committed lockfiles BEFORE stamping: the stamper + # rewrites package.json versions, never the lockfiles. [SWR-SEC-FROZEN-INSTALL] + - name: Install npm packages (frozen) + run: | + npm ci --prefix packages/lsp-profiler-ui + + # Stamp the tag into every version carrier (Cargo.toml, package.json, + # *.csproj, build-info.json) in the runner working tree only. The tagged + # commit keeps `0.0.0-dev`; nothing is committed back. + # [REL-VERSION] [SWR-VERSION-BUILD-STAMPING] - name: Stamp version from tag run: shipwright-version-stamp --tag "${GITHUB_REF_NAME}" --root . @@ -79,53 +118,125 @@ jobs: - name: Refresh lockfile for stamped versions run: cargo update --workspace - # Single root LICENSE-MIT is the source of truth; stage a copy into each - # crate so the published tarball ships the license text. [SWR-REL-LICENSE] - - name: Stage license into every crate + # Stage the root license into every crate (LICENSE-MIT, forced in by each + # crate's `include`) and npm package (LICENSE, which npm always packs). + # [REL-METADATA] [SWR-REL-LICENSE] + - name: Stage license into every package run: | set -euo pipefail - for crate in crates/*/; do - cp LICENSE-MIT "${crate}LICENSE-MIT" - done + for crate in crates/*/; do cp LICENSE-MIT "${crate}LICENSE-MIT"; done + for package in packages/*/; do cp LICENSE-MIT "${package}LICENSE"; done - # `make lint` lints the profiler UI package, so its tools come first. - - name: Install the profiler UI package - run: npm ci --prefix packages/lsp-profiler-ui - - # Full gate (lint + test + coverage + build) MUST pass before any publish. + # Full gate (fmt + lint + test + coverage + build) MUST pass before any publish. + # [REL-GATES] - name: CI gate run: make ci - # Re-run the supply-chain gate on the release path itself, against the - # stamped + freshly-locked dependency graph. supply-chain.yml only covers - # PR/push/weekly-cron, so an advisory disclosed after the last main merge - # would otherwise publish unblocked. Same deny.toml, so the policy is DRY. - # [SWR-SEC-VULN-GATE] + # Re-run the supply-chain gate against the stamped + freshly-locked graph, + # so an advisory disclosed since the last PR still blocks. [SWR-SEC-VULN-GATE] - name: Supply-chain gate (advisories + bans + licenses + sources) run: cargo deny check advisories bans licenses sources + # The npm half of the vuln gate: fail on a high advisory in anything the + # published packages pull in at runtime. [SWR-SEC-VULN-GATE] + - name: npm audit (runtime dependencies) + run: | + npm audit --omit=dev --audit-level=high --prefix packages/lsp-profiler-ui + + # Package every crate and verify each tarball builds on its own, against + # the other just-packaged crates. The tarballs are checksummed, attested + # and attached to the release; `cargo publish` uploads the same packaged + # source. [REL-GATES] [REL-PACKAGED-BUILD] [SWR-REL-CRATES] + - name: Package crates + run: | + set -euo pipefail + cargo package --workspace --locked --allow-dirty + mkdir -p "${DIST}" + cp target/package/*.crate "${DIST}/" + + - name: Package npm packages + run: | + set -euo pipefail + npm run build --prefix packages/lsp-profiler-ui + npm pack ./packages/lsp-profiler-ui --pack-destination "${DIST}" + + # One CycloneDX SBOM over the whole dependency graph. [SWR-SEC-SBOM] + - name: Generate SBOM + uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + path: . + format: cyclonedx-json + output-file: ${{ env.DIST }}/lspkit-sbom.cdx.json + upload-artifact: false + upload-release-assets: false + + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + # One SHA256SUMS over every release asset, cosign keyless-signed with this + # workflow's OIDC identity. [REL-GITHUB] [SWR-SEC-CHECKSUM] [SWR-SIGN-COSIGN] + - name: Write and sign SHA256SUMS + working-directory: ${{ env.DIST }} + run: | + set -euo pipefail + sha256sum -- * > SHA256SUMS + cosign sign-blob --yes --bundle SHA256SUMS.sigstore.json SHA256SUMS + + - name: Attest build provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: ${{ env.DIST }}/SHA256SUMS + + - name: Attest SBOM + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: ${{ env.DIST }}/SHA256SUMS + sbom-path: ${{ env.DIST }}/lspkit-sbom.cdx.json + + # Create the release on the tag (or refresh its assets on a re-run). + # A `-suffix` tag is a prerelease. [REL-GITHUB] [SWR-REL-GITHUB] + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then + gh release upload "${GITHUB_REF_NAME}" "${DIST}"/* --clobber + else + prerelease=() + case "${GITHUB_REF_NAME}" in *-*) prerelease=(--prerelease) ;; esac + gh release create "${GITHUB_REF_NAME}" "${DIST}"/* \ + --verify-tag --generate-notes --title "${GITHUB_REF_NAME}" "${prerelease[@]}" + fi + - name: Authenticate to crates.io via OIDC - id: auth - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 - - # Publish leaf crates (no internal deps) first, then dependents. `cargo - # publish` waits for index propagation before returning, so each dependent - # sees its just-published dependencies — no manual sleep. The per-crate - # wrapper is idempotent (skips already-published crates) so a re-run after a - # mid-batch failure is safe. [SWR-REL-CRATES] + id: crates_auth + uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6 + + # Dependencies before dependents; `cargo publish` waits for index + # propagation, so each dependent resolves its just-published dependencies. + # The wrapper skips already-published crates, so a re-run is safe. + # [REL-ORDER] [REL-OIDC] [SWR-REL-CRATES] - name: Publish crates in dependency order env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + CARGO_REGISTRY_TOKEN: ${{ steps.crates_auth.outputs.token }} run: | set -euo pipefail for crate in \ + lsp-profiler \ + lsp-profiler-dotnet \ lspkit \ - lspkit-vfs \ - lspkit-treesitter \ - lspkit-sidecar \ - lspkit-config \ - lspkit-live \ - lspkit-mcp \ lspkit-server; do bash scripts/cargo-publish-or-skip.sh "${crate}" done + + # Publish the exact tarballs signed above. setup-node writes a placeholder + # NODE_AUTH_TOKEN; blank it so npm takes the OIDC path. [REL-OIDC] [SWR-REL-NPM] + - name: Publish npm packages + env: + NODE_AUTH_TOKEN: "" + NPM_CONFIG_PROVENANCE: "true" # [SWR-SEC-PROVENANCE] + run: | + set -euo pipefail + for tarball in "${DIST}"/*.tgz; do + bash scripts/npm-publish-or-skip.sh "${tarball}" + done diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index 61dfec3..b6a1845 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -1,12 +1,12 @@ name: supply-chain # Vulnerability + license gate across the dependency graph. [SWR-SEC-VULN-GATE] -# Runs on every PR/push and on a weekly schedule to catch newly disclosed advisories. +# Runs on PRs to `main` and on a weekly schedule to catch newly disclosed +# advisories; release.yml re-runs cargo-deny on the tagged SHA before it +# publishes. Pushes to `main` trigger nothing. [REL-REGIME] [CI-WORKFLOWS] on: pull_request: branches: [main] - push: - branches: [main] schedule: - cron: "0 6 * * 1" # Mondays 06:00 UTC @@ -21,6 +21,8 @@ permissions: jobs: cargo-deny: name: cargo-deny (advisories + licenses + bans + sources) + # Dependabot PRs are swept into `dependabot-upgrades`. [GITHUB-DEPENDABOT] + if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -32,7 +34,9 @@ jobs: command: check advisories bans licenses sources osv-scanner: - name: osv-scanner (Cargo.lock) + name: osv-scanner (Cargo.lock + npm lockfiles) + # Dependabot PRs are swept into `dependabot-upgrades`. [GITHUB-DEPENDABOT] + if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -45,3 +49,25 @@ jobs: with: scan-args: |- --lockfile=Cargo.lock + --lockfile=packages/lsp-profiler-ui/package-lock.json + + # Workflow static analysis: unpinned actions, over-broad tokens, template + # injection, cache poisoning, spoofable bot checks. Same settings as + # Shipwright's lint-supply-chain.yml. [SWR-GATE-ACTION-PINNING] [SWR-GATE-TOKEN-PRIVILEGE] + zizmor: + name: zizmor (workflow static analysis) + # Dependabot PRs are swept into `dependabot-upgrades`. [GITHUB-DEPENDABOT] + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 + with: + persona: pedantic + min-severity: medium + # No advisory upload: the gate stays read-only. + advanced-security: false + online-audits: false diff --git a/.gitignore b/.gitignore index cf6aa36..4ef04d2 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ Cargo.lock.bak # at release time; never committed). [SWR-VERSION-BUILD-STAMPING] build-info.json crates/*/LICENSE-MIT +packages/*/LICENSE +# Release assets (crate + npm tarballs, SBOM, signed SHA256SUMS). [SWR-REL-GITHUB] +/dist/ # ============================================================================= # PROFILER (lsp-profiler crates, .NET engine, UI package) diff --git a/AGENTS.md b/AGENTS.md index 48aaceb..9ab46df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,2 +1,75 @@ -@CLAUDE.md +# LSP Toolkit — Agent Instructions + +Reusable Rust components for building language servers, plus the shared CPU and memory profiler every Nimblesite language server mounts. Consumed by SharpLsp, Deslop, Basilisk and lql-lsp-rust. Spec: [docs/specs/lspkit-spec.md](docs/specs/lspkit-spec.md). + +**Languages:** Rust, TypeScript 7 (`packages/`), C# and F# (`dotnet/`). **Build:** `make ci` · **Test:** `make test` · **Lint:** `make lint` · **Format:** `make fmt`. Repo-specific targets sit below the marker in the [Makefile](Makefile). Debug one test with `cargo test -- --nocapture`. + +## Workflow + +- 🛑 **Never kill a VS Code process**, including browser-hosted instances. +- 🛑 **No git commands** unless the user asks; CI handles git. Never add `Co-Authored-By` or agent attribution. +- **Don't ask the user questions.** Use judgment, record assumptions, keep replies to a status line. Mark the one critical point with an emoji. +- **Subagents are illegal.** Delegate to `pi` agents (Kimi → GLM → DeepSeek, latest models). You own their output. +- **Use Too Many Cooks** for file locks and agent messaging when available. +- **Never weaken, bypass or narrow a CI gate**, threshold or timeout. A red check means the code is wrong. +- **Don't close GitHub issues** unless the user asks. Keep GitHub Actions spend minimal. +- **Release-only regime** ([REL-REGIME](docs/specs/lspkit-spec.md#release--publishing)). CI runs on PRs to `main` only; a push to `main` triggers nothing. Shipping happens only when a `v*` tag (or `gh release create vX.Y.Z`) runs `release.yml`, which publishes every crate and npm package via OIDC. Source versions stay `0.0.0-dev`. +- **Auto-memory is off.** Durable rules are reviewed changes to this file. +- **Clean up after yourself**: temporary files, build output, disk space. + +## Code (every language) + +- **Zero duplication.** Run [Deslop](https://deslop.live/docs/for-ai/) `find-similar` before writing code (fused ≥ 0.85 → reuse) and `top-offenders` after. Move code, never copy it. +- **All lints on, all at error.** Never suppress a warning (`#[allow]`, `oxlint-disable`, `#pragma warning disable`); fix the code. A rule is off only with a written reason in its config. +- **No regex or string matching on code or structured data.** Use tree-sitter or a real parser. +- **Functional style.** `Result`/`Option` over exceptions and null, expressions over statements, pure functions, early returns. +- **No placeholders, silent no-ops or legacy code.** Silently broken code becomes an explicit error plus a failing test, reported to the user. +- **Files < 500 lines, functions < 20 lines.** Named constants, not literals. +- **Models are generated with [typeDiagram](https://typediagram.dev/docs/language-reference.html)**, never hand-written or committed. +- **Global state lives in one `state.rs` per crate** (or the language's equivalent). +- **Dependency versions stay in sync** across manifests, `.github/workflows/` and `.devcontainer/`. +- **Structured logging only**: `tracing` (Rust), the package `logger` module (TypeScript), `Microsoft.Extensions.Logging` (.NET). Never log PII, secrets, file contents or user paths. +- **Bug fixes follow [fix-bug](.agents/skills/fix-bug/SKILL.md)**: failing test first. Skills live in `.agents/skills/`; run `node scripts/link-skills.mjs` after adding one. + +## Rust + +- No `unwrap()`/`expect()` in production; tests may `expect` with a message. +- No `panic!`/`todo!`/`unimplemented!`/`unreachable!`, no `unsafe`. Public items carry `///` docs. +- `thiserror` in libraries, `anyhow` only in binaries. Pattern matching over casts; iterator chains over loops. + +## TypeScript + +- 🛑 **TypeScript 7 only** (`typescript@^7`). Never pin an older compiler or add a tool that needs one. +- **Lint with oxlint** (`--type-aware`, tsgolint) using [packages/.oxlintrc.json](packages/.oxlintrc.json), the one config for every package. ESLint and typescript-eslint don't support TypeScript 7. +- `strict` tsconfig. No `any`, no `!` non-null assertions, no `as` casts, no `@ts-ignore`. Narrow with type guards. +- Explicit return types and member accessibility. `async`/`await`, never `.then()`. No floating promises. +- VS Code extension tests drive the editor through `vscode.commands.executeCommand`, never provider methods. + +## C# and F# + +- Latest .NET, nullable enabled, `TreatWarningsAsErrors`, analyzers at the highest level, all in [dotnet/Directory.Build.props](dotnet/Directory.Build.props). +- No `!` null-forgiving, `as` casts or `dynamic`; use pattern matching. Return results instead of throwing. +- Format C# with CSharpier and F# with Fantomas. + +## Testing + +- **Coarse E2E, black-box only**, against real binaries. No fake LSP or MCP. Unit tests only to isolate a problem. +- **Many user interactions per test, many assertions per interaction.** No `assert!(true)` and no swallowed errors. +- **Never delete or skip a failing test or weaken an assertion.** Add failing tests for broken functionality. +- **100% coverage target.** `make test` is fail-fast and enforces `coverage-thresholds.json`, which only ratchets up. +- **Deterministic.** No `sleep`, timing dependencies or random state. + +## Docs and spec IDs + +- Specs live in `docs/specs/`, plans with TODO checklists in `docs/plans/`. Read the spec before starting a feature. +- Spec IDs are hierarchical and non-numeric (`[GROUP-TOPIC]`, `[GROUP-TOPIC-DETAIL]`). Code and tests cite them so `grep [GROUP-` finds spec, code and tests. +- Diagrams are Mermaid; models are typeDiagram. Specs are for humans first; no forced line wraps. + +## Profiler + +One profiler for every lspkit language server: a language plugs in a runtime crate, and everything else is shared. Read [LSP-PROFILER-SPEC](docs/specs/LSP-PROFILER-SPEC.md) and [LSP-PROFILER-PLAN](docs/plans/LSP-PROFILER-PLAN.md) first. + +- `[PROF-INVARIANTS]` are hard rules. Every `[PROF-…]` citation must resolve to one spec heading; `make lint` checks this. +- The wire model is `docs/models/profiler.td`, generated by `crates/lsp-profiler/build.rs`. +- `make test` builds the .NET engine first and needs the `dotnet` SDK. diff --git a/Cargo.lock b/Cargo.lock index ef67f03..12b5a8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,7 +272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -317,15 +317,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "futures" version = "0.3.32" @@ -774,7 +765,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lsp-profiler" -version = "0.0.1" +version = "0.0.0-dev" dependencies = [ "async-trait", "humantime", @@ -792,7 +783,7 @@ dependencies = [ [[package]] name = "lsp-profiler-dotnet" -version = "0.0.1" +version = "0.0.0-dev" dependencies = [ "async-trait", "lsp-profiler", @@ -819,73 +810,38 @@ dependencies = [ [[package]] name = "lspkit" -version = "0.0.1" +version = "0.0.0-dev" dependencies = [ "async-trait", + "dashmap", "futures-core", "futures-util", - "thiserror", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lspkit-config" -version = "0.0.1" -dependencies = [ + "rmp-serde", + "ropey", "serde", - "tempfile", - "thiserror", - "toml", - "tracing", -] - -[[package]] -name = "lspkit-live" -version = "0.0.1" -dependencies = [ - "async-trait", - "futures-util", - "lspkit", - "notify", "serde_json", "tempfile", "thiserror", "tokio", "tokio-util", + "toml", "tracing", -] - -[[package]] -name = "lspkit-mcp" -version = "0.0.1" -dependencies = [ - "async-trait", - "futures-core", - "futures-util", - "lspkit", - "rmcp", - "serde", - "serde_json", - "thiserror", - "tokio", - "tokio-util", - "tracing", + "tracing-subscriber", + "tree-sitter", + "tree-sitter-rust", ] [[package]] name = "lspkit-server" -version = "0.0.1" +version = "0.0.0-dev" dependencies = [ "async-trait", + "futures-core", "futures-util", "lsp-types", "lspkit", - "lspkit-live", - "lspkit-sidecar", - "lspkit-vfs", + "notify", + "rmcp", "salsa", "serde", "serde_json", @@ -897,39 +853,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "lspkit-sidecar" -version = "0.0.1" -dependencies = [ - "rmp-serde", - "serde", - "serde_json", - "tempfile", - "thiserror", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "lspkit-treesitter" -version = "0.0.1" -dependencies = [ - "dashmap", - "thiserror", - "tree-sitter", - "tree-sitter-rust", -] - -[[package]] -name = "lspkit-vfs" -version = "0.0.1" -dependencies = [ - "dashmap", - "ropey", - "thiserror", -] - [[package]] name = "matchers" version = "0.2.0" @@ -954,25 +877,26 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "notify" -version = "8.2.0" +version = "8.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +checksum = "82f812702fbc75d35416d3f884e04925be7e9498f4eceea2e24ba34559284674" dependencies = [ "bitflags 2.11.1", - "fsevent-sys", "inotify", "kqueue", "libc", "log", "mio", "notify-types", + "objc2-core-foundation", + "objc2-core-services", "walkdir", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -999,7 +923,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1041,6 +965,16 @@ dependencies = [ "objc2", ] +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -1278,14 +1212,15 @@ dependencies = [ [[package]] name = "rmcp" -version = "3.5.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5023804cbe0304cbe06197ac16e645a0a0d0ffa2953bf735c15c2fa0213350" +checksum = "f8be15815186873ea063bf005011d16c356804973489e40c39116ae3a2878d1f" dependencies = [ "base64", "chrono", "futures", "indexmap", + "libc", "pastey", "pin-project-lite", "rmcp-macros", @@ -1301,9 +1236,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "3.5.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1da498700030856ba63114bceefa30ba21f5e278cb975e5b9de3529fe842b40" +checksum = "8b0748e054a5f5598b3213cf72099ae6b2745ba4d996c59ccf455150a7dbb839" dependencies = [ "darling", "proc-macro2", @@ -1357,7 +1292,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1576,7 +1511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1664,10 +1599,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1717,9 +1652,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.3" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -1728,7 +1663,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2092,7 +2027,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2202,15 +2137,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2220,23 +2146,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -2246,54 +2155,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index 03d35c0..f96524a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,20 +3,14 @@ resolver = "2" members = [ "crates/lspkit", "crates/lspkit-server", - "crates/lspkit-vfs", - "crates/lspkit-live", - "crates/lspkit-mcp", - "crates/lspkit-config", - "crates/lspkit-treesitter", - "crates/lspkit-sidecar", "crates/lsp-profiler", "crates/lsp-profiler-dotnet", ] -# Parked profiler runtimes: kept with the profiler, not built yet. -exclude = ["crates/lsp-profiler-helper", "crates/lsp-profiler-python"] +# Parked profiler runtime: kept with the profiler, not built yet. +exclude = ["crates/lsp-profiler-python"] [workspace.package] -version = "0.0.1" +version = "0.0.0-dev" edition = "2021" rust-version = "1.89" license = "MIT" @@ -140,6 +134,9 @@ ropey = "1" # Filesystem notify = "8" +# Config +toml = "1.1" + # Errors / tracing thiserror = "2" tracing = "0.1" @@ -153,19 +150,13 @@ sysinfo = { version = "0.39.3", default-features = false, features = ["system", tempfile = "3" url = "2" -# Tree-sitter (optional, in lspkit-treesitter only) +# Tree-sitter (optional: the `treesitter` feature of lspkit) tree-sitter = "0.27" # Internal workspace dependencies. # Versions are the `0.0.0-dev` placeholder; the release stamps the tag into # both `[workspace.package].version` and these requirements. [SWR-VERSION-BUILD-STAMPING] -lspkit = { version = "0.0.1", path = "crates/lspkit" } -lspkit-server = { version = "0.0.1", path = "crates/lspkit-server" } -lspkit-vfs = { version = "0.0.1", path = "crates/lspkit-vfs" } -lspkit-live = { version = "0.0.1", path = "crates/lspkit-live" } -lspkit-mcp = { version = "0.0.1", path = "crates/lspkit-mcp" } -lspkit-config = { version = "0.0.1", path = "crates/lspkit-config" } -lspkit-treesitter = { version = "0.0.1", path = "crates/lspkit-treesitter" } -lspkit-sidecar = { version = "0.0.1", path = "crates/lspkit-sidecar" } -lsp-profiler = { version = "0.0.1", path = "crates/lsp-profiler" } -lsp-profiler-dotnet = { version = "0.0.1", path = "crates/lsp-profiler-dotnet" } +lspkit = { version = "0.0.0-dev", path = "crates/lspkit" } +lspkit-server = { version = "0.0.0-dev", path = "crates/lspkit-server" } +lsp-profiler = { version = "0.0.0-dev", path = "crates/lsp-profiler" } +lsp-profiler-dotnet = { version = "0.0.0-dev", path = "crates/lsp-profiler-dotnet" } diff --git a/Claude.md b/Claude.md index 29e4afd..43c994c 100644 --- a/Claude.md +++ b/Claude.md @@ -1,159 +1 @@ - -# LSP Toolkit - -**Repo:** https://github.com/Nimblesite/lspkit - -Reusable Rust components for building LSPs and related tooling, including the shared CPU and memory profiler that every Nimblesite language server mounts. Consumed by: - -- [lql-lsp-rust](/Users/christianfindlay/Documents/Code/DataProvider/Lql/lql-lsp-rust) -- [Basilisk](/Users/christianfindlay/Documents/Code/Basilisk) -- [Deslop](/Users/christianfindlay/Documents/Code/Deslop) -- [SharpLsp](/Users/christianfindlay/Documents/Code/SharpLsp) - -**Language:** Rust · **Build:** `make ci` · **Test:** `make test` · **Lint:** `make lint` - -To debug a single test: `cargo test -- --nocapture` (not a make target). - -> ⚠️ Killing a VSCode process — even in the browser — is forbidden. - -## Quality Bar - -Code must pass review at Google / Meta / Microsoft. No exceptions, not even for one line. - -## Token Discipline - -Check file size first. `Grep` over `Read`. Use `offset`/`limit`. Smallest diff that solves the problem. Delete dead code, unused imports, stale comments. Call out irrelevant context before proceeding. - -## IPC - -Processes communicate via IPC. Generate IPC model code with [typeDiagram](https://typediagram.dev/docs/language-reference.html). Generated model code is gitignored, never committed. - -## Spec IDs - -Hierarchical, non-numeric: `[GROUP-TOPIC]` / `[GROUP-TOPIC-DETAIL]` (e.g. `[PARSE-CSHARP-NORMALIZE]`, `[RANK-SCORE]`). Same-group sections sit adjacent in the spec. Code and tests reference the ID in a comment so `grep [PARSE-` walks spec → code → tests. - -## Hard Rules — Universal - -- **Files < 500 lines, functions < 20 lines.** Refactor when over. -- **DRY.** Search before writing. Move code, don't copy. -- **No git commands.** CI handles git. -- **Regex on source code = illegal.** Use tree-sitter. -- **No placeholders, no silent no-ops.** Return `Result`. Panics are bugs. -- **No legacy code.** Legacy = deleted. Copying files = illegal; move them. -- **Centralize global state** in a single `state.rs` (or its equivalent in any other language) per crate. -- **Dependency versions in `Cargo.toml`, CI workflows, and `.devcontainer/` stay in sync.** -- **Mandatory bug fix process:** [.agents/skills/fix-bug/SKILL.md](.agents/skills/fix-bug/SKILL.md). -- **Skills live in `.agents/skills//`**, the location every agent reads; `.claude/skills/` is a link to each for Claude Code. Add a skill there, never under `.claude/`, then run `node scripts/link-skills.mjs` (`make lint` checks the links; the script also repairs a Windows checkout without symlinks). - -## Hard Rules — Rust - -- No `unwrap()` / `expect()` in production (tests may `expect` with a message). -- No `panic!` / `todo!` / `unimplemented!` / `unreachable!`. -- No `unsafe {}`. Workspace lint: `unsafe_code = "deny"`. -- All public items have `///` doc comments. Workspace lint: `missing_docs = "deny"`. -- `thiserror` for library errors; `anyhow` only in binaries. -- Pattern matching over casting. Expressions over statements. Iterator chains over imperative loops. -- Early return with `?`. -- Descriptive variable names — no single letters except in closures. -- No `#[allow(clippy::...)]` suppressions. Fix the underlying code. - -## Logging - -- `tracing` + `tracing-subscriber` only. Never `println!` / `eprintln!` for diagnostics. -- Log entry/exit of significant operations. Levels: `error | warn | info | debug | trace`. -- Structured fields, not string interpolation: `tracing::info!(file_count = 42, lang = "csharp")`. -- Never log file contents, user paths, or secrets. Log counts and hashes. -- The CLI's user-facing output is not a log — it goes to stdout via the renderer; diagnostics go to `tracing`. - -## Testing - -- **Coarse E2E only.** Drive the CLI end-to-end against fixtures; assert against rendered output. Never reach into internals. -- **Real binaries only.** Testing UI/extensions against a fake LSP or MCP is illegal — build and install the real binaries first. -- **`make test` is fail-fast.** Never `--no-fail-fast`. -- **`make test` enforces coverage** from `coverage-thresholds.json` at the repo root. Threshold ratchets up only (-1% rounding tolerance). Not env vars, not CI YAML. -- **Never delete a failing test. Never weaken an assertion.** Add more failing tests for broken functionality. -- **Meaningful assertions only.** `assert!(true)` is illegal. No try/catch that swallows errors and asserts success. -- **Deterministic.** No `sleep`, no timing dependencies, no random state. - -## Profiler - -`lsp-profiler` is one CPU and memory profiler for every language server built on lspkit. A language plugs in a runtime crate; the service, the wire, the LSP surface and the VS Code UI library are shared and never change per language. - -**Spec:** [docs/specs/LSP-PROFILER-SPEC.md](docs/specs/LSP-PROFILER-SPEC.md). **Plan:** [docs/plans/LSP-PROFILER-PLAN.md](docs/plans/LSP-PROFILER-PLAN.md). Every `[PROF-…]` citation must resolve to exactly one spec heading (`make lint` runs `scripts/profiler-spec-citations.mjs`). - -- The `[PROF-INVARIANTS]` are hard: nothing above `CpuBackend` / `MemoryBackend` names a tool, a language or a runtime; the Rust layer computes everything the UI shows; standard LSP first; no scraping of console output. -- The profiler crates carry their own stricter `[lints]` (no `unwrap`, `expect`, `panic`, indexing, `as` casts or `print!` on any path). -- Errors are `thiserror` types mapped to `[PROF-ERRORS]` codes; a failing tool is a classified error, never a crash. -- The wire model is `docs/models/profiler.td`; `crates/lsp-profiler/build.rs` generates `wire_generated.rs` and the UI's `wire-generated.ts` with typeDiagram (gitignored). -- `make test` builds the .NET engine and its fixture first (`dotnet` SDK required). - -| Path | What | -|---|---| -| `crates/lsp-profiler` | The core: sessions, aggregation, exports, memory scoring, `ProfilerService`, `commands` | -| `crates/lsp-profiler-dotnet` | The .NET runtime: process classification, engine driver, CPU and memory backends | -| `dotnet/LspProfiler.DotNet.Engine` | The C# engine: EventPipe CPU sampling and heap snapshots, NDJSON out | -| `dotnet/fixtures/HotLeakApp` | The .NET test target: a hot loop and a static-rooted leak | -| `dotnet/fixtures/HotLeakAppFs` | Its F# twin: records, a union and a module value, which are IL shapes only F# emits | -| `packages/lsp-profiler-ui` | The VS Code UI library (`make profiler-ui`, `make profiler-ui-test`) | -| `crates/lsp-profiler-python`, `crates/lsp-profiler-helper` | Parked runtimes: excluded from the workspace | - -## Multi-Agent Coordination (Too Many Cooks) - -If the TMC server is available: register on start (name, intent, files), lock files before editing, broadcast your plan, check messages periodically, release locks when done. Never edit a locked file — wait or take another approach. - -## Build Commands - -Cross-platform GNU Make. Exactly 7 standard targets: - -```bash -make build # cargo build --release --workspace -make test # FAIL-FAST tests + coverage + threshold (ONLY test entry point) -make lint # cargo clippy -- -D warnings (no formatting) -make fmt # cargo fmt --all (pass CHECK=1 for read-only check) -make clean # cargo clean -make ci # lint + test + build (full CI simulation) -make setup # post-create dev environment setup (devcontainer hook) -``` - -Repo-specific helpers live in the `Repo-Specific Targets` section of the Makefile (e.g. `make doc`). `make test` runs `cargo llvm-cov` with `--fail-fast`, asserts measured ≥ threshold from `coverage-thresholds.json`, and exits non-zero on any failure. To debug a single test, invoke `cargo test -- --nocapture` directly — that is not a Makefile target. - -`make fmt`, `make lint`, `make test` are three separate non-overlapping targets. - -## Repo Structure - -``` -lspkit/ -├── CLAUDE.md # canonical agent instructions (this file) -├── AGENTS.md # pointer → CLAUDE.md -├── Makefile # 7 standard targets + Repo-Specific section -├── rustfmt.toml -├── coverage-thresholds.json # single source of truth for thresholds -├── opencode.json # OpenCode pointer -├── .cursorrules / .windsurfrules # pointers -├── .agents/skills/ # ci-prep, code-dedup, fix-bug, spec-check, -│ # submit-pr, upgrade-packages -├── .claude/skills/ # a link to each skill, for Claude Code -├── .clinerules/ # Cline pointer -├── .devcontainer/devcontainer.json -├── .github/ -│ ├── copilot-instructions.md # pointer -│ ├── pull_request_template.md -│ └── workflows/ci.yml -├── .vscode/extensions.json -├── docs/ -│ ├── specs/ # behaviour & requirements (spec IDs live here) -│ └── plans/ # how-to-achieve-goals + TODO checklists -├── crates/ # workspace crates (lspkit, lspkit-server, -│ # lspkit-vfs, lspkit-live, lspkit-mcp, -│ # lspkit-config, lspkit-treesitter, -│ # lspkit-sidecar, lsp-profiler, -│ # lsp-profiler-dotnet — see docs/specs/) -├── dotnet/ # the profiler's .NET engine + test fixture -└── packages/ # editor packages (lspkit-debug, lsp-profiler-ui) -``` - -The canonical spec for the eight crates and the "one engine, two surfaces" pattern lives in [docs/specs/lspkit-spec.md](docs/specs/lspkit-spec.md). - -### Cargo workspace lints (when Cargo.toml is created) - -The Rust hard rules above are enforced via `[workspace.lints.rust]` and `[workspace.lints.clippy]` in `Cargo.toml`. The required deny-list is in [REPO-STANDARDS-SPEC](../AgentPMOWorkflow/docs/specs/REPO-STANDARDS-SPEC.md) under `[LINT-RUST]` and mirrored in `agent-pmo-skill/templates/linting/cargo-workspace-lints.toml`. Copy that block into the workspace `Cargo.toml` when scaffolding crates. +@AGENTS.md diff --git a/Makefile b/Makefile index fd2bbc0..465e07d 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,8 @@ endif # thresholds all have to fail closed). This regression cannot be dropped. $(MAKE) _coverage_check_test $(MAKE) _typediagram_test + npm run build --prefix packages/lsp-profiler-ui + npm run test:debug --prefix packages/lsp-profiler-ui ## lint: Run all linters/analyzers (read-only). Does NOT format. lint: @@ -69,6 +71,7 @@ lint: $(MAKE) _profiler_spec_check node scripts/link-skills.mjs --check npm run lint --prefix packages/lsp-profiler-ui + $(MAKE) _dotnet_build ## fmt: Format all code in-place. Pass CHECK=1 for read-only check (CI use). fmt: @@ -115,11 +118,15 @@ help: ## The profiler's .NET engine and the fixture its e2e tests profile (Release). ## lsp-profiler-dotnet's tests run both. [PROF-TESTING] -_profiler_dotnet: +_profiler_dotnet: _dotnet_build + dotnet test -c Release dotnet/LspProfiler.DotNet.Engine.Tests + +## Every .NET project, analyzers at latest-all, warnings as errors (dotnet/Directory.Build.props). +_dotnet_build: dotnet build -c Release dotnet/LspProfiler.DotNet.Engine dotnet build -c Release dotnet/fixtures/HotLeakApp dotnet build -c Release dotnet/fixtures/HotLeakAppFs - dotnet test -c Release dotnet/LspProfiler.DotNet.Engine.Tests + dotnet build -c Release dotnet/LspKit.Sidecar ## The profiler's gitignored wire model ([PROF-WIRE-GEN]). build.rs regenerates ## it on every build; rustfmt needs it before anything has been built. @@ -142,6 +149,9 @@ _profiler_spec_check: _coverage_check_test: node scripts/coverage-gate.test.mjs --make "$(MAKE)" --shell "$(POSIX_SHELL)" +# A `::` key (e.g. `lspkit::vfs`) gates one module of a crate: +# `src/.rs` plus `src//`, so absorbed crates keep their own +# floors after consolidation. [CRATE-LAYOUT-MODULES] # Construct the escaped SF backslash from character codes so Windows argv # quoting cannot consume it before awk normalizes native source markers. _coverage_check: SHELL := $(POSIX_SHELL) @@ -158,7 +168,7 @@ _coverage_check: for CRATE in $$CRATES; do \ THRESHOLD=$$(jq -r --arg c "$$CRATE" '.per_crate[$$c]' "$(COVERAGE_THRESHOLDS_FILE)" | tr -d '\r') || exit 1; \ if [ -z "$$THRESHOLD" ] || [ "$$THRESHOLD" = "null" ]; then echo "FAIL: $$CRATE — threshold missing in $(COVERAGE_THRESHOLDS_FILE)"; exit 1; fi; \ - COV=$$(awk -v crate="$$CRATE" 'BEGIN{lh=0;lf=0;in_block=0} /^SF:/{gsub(sprintf("%c%c",92,92),"/"); sub(/^SF:/,"SF:/"); in_block=(index($$0,"/crates/" crate "/src/")>0)?1:0} in_block&&/^LH:/{sub(/^LH:/,""); lh+=$$0} in_block&&/^LF:/{sub(/^LF:/,""); lf+=$$0} /^end_of_record/{in_block=0} END{printf "%d %d", lh, lf}' lcov.info) || exit 1; \ + COV=$$(awk -v crate="$$CRATE" 'BEGIN{lh=0;lf=0;in_block=0;n=index(crate,"::");dir=(n>0)?substr(crate,1,n-1):crate;mod=(n>0)?substr(crate,n+2):"";gsub(/::/,"/",mod);tree="/crates/" dir "/src/" mod;file=(n>0)?tree ".rs":tree;tree=(n>0)?tree "/":tree} /^SF:/{gsub(sprintf("%c%c",92,92),"/"); sub(/^SF:/,"SF:/"); in_block=(index($$0,tree)>0||index($$0,file)>0)?1:0} in_block&&/^LH:/{sub(/^LH:/,""); lh+=$$0} in_block&&/^LF:/{sub(/^LF:/,""); lf+=$$0} /^end_of_record/{in_block=0} END{printf "%d %d", lh, lf}' lcov.info) || exit 1; \ LH=$${COV% *}; LF=$${COV#* }; \ if [ "$$LF" -eq 0 ]; then echo "FAIL: $$CRATE — no lines in lcov.info"; FAILED=1; continue; fi; \ PCT=$$(awk "BEGIN{printf \"%.2f\", $$LH/$$LF*100}"); \ diff --git a/README.md b/README.md index 8ba54ef..81c46b7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lspkit -A generic Rust workspace of cross-cutting building blocks for LSP and MCP servers, published as `lspkit-*` crates. +A generic Rust workspace of cross-cutting building blocks for LSP and MCP servers, published as four crates: `lspkit`, `lspkit-server`, `lsp-profiler` and `lsp-profiler-dotnet`. ## What it is @@ -8,25 +8,20 @@ The headline contribution is the **"one engine, two surfaces"** contract: a sing | Crate | Responsibility | |---|---| -| `lspkit` | Façade. `EngineApi` trait + supporting types. Optional `tracing-setup` feature. | -| `lspkit-server` | Hand-rolled JSON-RPC LSP server scaffolding built on `lsp-types`. Capability builder, URI helpers, diagnostics fan-out, progress, cancellation. | -| `lspkit-vfs` | Concurrent open-document store with rope-based incremental edits and position-encoding negotiation. | -| `lspkit-live` | File-watcher → debouncer → single-flight scheduler → session lifecycle. | -| `lspkit-mcp` | `rmcp` adapter that mounts an `EngineApi` impl as MCP tools, resources, and prompts. | -| `lspkit-config` | Layered TOML loader generic over the consumer's config struct. | -| `lspkit-treesitter` | `LanguageParser` trait + content-hash-keyed parse cache. Bundles no grammars. | -| `lspkit-sidecar` | Framed-IPC primitives + lifecycle/health/respawn. Pure transport. | +| `lspkit` | Façade. `EngineApi` trait + supporting types, plus the `config`, `vfs` and `sidecar` modules and the `treesitter` module behind the `treesitter` feature. Optional `tracing-setup` feature. | +| `lspkit-server` | Hand-rolled JSON-RPC LSP server scaffolding built on `lsp-types`, plus the `live` module and the `mcp` adapter behind the `mcp` feature. Capability builder, URI helpers, diagnostics fan-out, progress, cancellation. | | `lsp-profiler` | Language-neutral CPU and memory profiler for language servers: sessions, hotspots, flame graphs, speedscope/V8 exports, heap snapshots, leak scoring, one JSON-RPC protocol. [Spec](docs/specs/LSP-PROFILER-SPEC.md). | | `lsp-profiler-dotnet` | The profiler's .NET runtime: EventPipe CPU sampling and heap snapshots through the engine in `dotnet/`. | +`lsp-profiler-python` is parked and unpublished; it hosts the `lsp-profiler-helper` binary and depends on `lsp-profiler`. + ## Editor packages Reusable editor-side building blocks that are not Rust crates live under `packages/`: | Package | Responsibility | |---|---| -| [`@nimblesite/lspkit-debug`](packages/lspkit-debug) | Language-neutral VS Code **Debug Adapter Protocol** glue + integration-test harness (adapter resolution, zero-config launch defaults, save-or-reject, profiling-launch hooks, and a `customRequest`-driven DAP test client/assertions). Written once for every Nimblesite language extension (Osprey · `lldb-dap`, SharpLsp · `netcoredbg`, Basilisk · `debugpy`) instead of per-language. Fully dependency-injected (no static `import "vscode"`), 100% unit-test coverage. | -| [`@nimblesite/lsp-profiler-ui`](packages/lsp-profiler-ui) | Editor-neutral VS Code UI for the profiler protocol: processes view, CPU heat map, flame graph, memory dashboard, retention graph, autopilot. | +| [`@nimblesite/lsp-profiler-ui`](packages/lsp-profiler-ui) | Editor-neutral VS Code UI for the profiler protocol: processes view, CPU heat map, flame graph, memory dashboard, retention graph, autopilot. Its [`debug`](packages/lsp-profiler-ui/src/debug) module is the language-neutral **Debug Adapter Protocol** glue and integration-test harness (adapter resolution, zero-config launch defaults, save-or-reject, profiling-launch hooks, a `customRequest`-driven DAP test client and assertions), dependency-injected with no static `import "vscode"`. | ## Status diff --git a/coverage-thresholds.json b/coverage-thresholds.json index 29734a5..32d0e84 100644 --- a/coverage-thresholds.json +++ b/coverage-thresholds.json @@ -1,16 +1,16 @@ { "_agent_pmo": "74cf183", - "_doc": "Single source of truth for per-crate line-coverage thresholds. Read by `_coverage_check` inside `make test`. Thresholds are monotonically increasing — only ratchet UP, never down. `make test` (and therefore CI) fails if ANY listed crate drops below its threshold.", + "_doc": "Single source of truth for per-crate line-coverage thresholds. Read by `_coverage_check` inside `make test`. Thresholds are monotonically increasing — only ratchet UP, never down. `make test` (and therefore CI) fails if ANY listed crate drops below its threshold. A `::` key gates one module (`src/.rs` + `src//`); each module absorbed by [CRATE-LAYOUT-MODULES] keeps its old crate's floor.", "per_crate": { "lsp-profiler": 94, "lsp-profiler-dotnet": 92, - "lspkit": 100, - "lspkit-config": 100, - "lspkit-live": 94, - "lspkit-mcp": 100, - "lspkit-server": 98, - "lspkit-sidecar": 99, - "lspkit-treesitter": 95, - "lspkit-vfs": 100 + "lspkit": 99, + "lspkit::config": 100, + "lspkit::sidecar": 99, + "lspkit::treesitter": 95, + "lspkit::vfs": 100, + "lspkit-server": 97, + "lspkit-server::live": 94, + "lspkit-server::mcp": 100 } } diff --git a/crates/lsp-profiler-dotnet/Cargo.toml b/crates/lsp-profiler-dotnet/Cargo.toml index c748559..0e7aec5 100644 --- a/crates/lsp-profiler-dotnet/Cargo.toml +++ b/crates/lsp-profiler-dotnet/Cargo.toml @@ -9,6 +9,10 @@ repository.workspace = true readme.workspace = true keywords = ["profiler", "lsp", "flamegraph", "memory", "language-server"] categories = ["development-tools::profiling"] +# Lean tarball: ship sources + the release-staged license only. LICENSE-MIT is +# gitignored (staged at publish time), so `include` forces it into the package. +# [PROF-WIRE-PACKAGED] +include = ["src/**/*", "LICENSE-MIT"] # The profiler's own lint set, stricter than the lspkit workspace's # ([PROF-INVARIANTS]): no unwrap, expect, panic, indexing or `as` casts. diff --git a/crates/lsp-profiler-helper/Cargo.toml b/crates/lsp-profiler-python/Cargo.toml similarity index 67% rename from crates/lsp-profiler-helper/Cargo.toml rename to crates/lsp-profiler-python/Cargo.toml index 73beb1b..66800c4 100644 --- a/crates/lsp-profiler-helper/Cargo.toml +++ b/crates/lsp-profiler-python/Cargo.toml @@ -1,8 +1,11 @@ +# Parked: not a workspace member and not built yet. The Python runtime's +# library sources are still being ported; its elevated py-spy helper is a +# `[[bin]]` of this crate. [CRATE-LAYOUT-HELPER] [package] -name = "lsp-profiler-helper" +name = "lsp-profiler-python" version.workspace = true edition.workspace = true -description = "Elevated helper binary for py-spy memory reading on macOS" +description = "Python runtime for lsp-profiler, with the elevated py-spy helper binary for macOS" license.workspace = true repository.workspace = true rust-version.workspace = true @@ -10,7 +13,7 @@ publish = false [[bin]] name = "lsp-profiler-helper" -path = "src/main.rs" +path = "src/bin/lsp-profiler-helper/main.rs" [dependencies] basilisk-common.workspace = true diff --git a/crates/lsp-profiler-helper/build.rs b/crates/lsp-profiler-python/build.rs similarity index 100% rename from crates/lsp-profiler-helper/build.rs rename to crates/lsp-profiler-python/build.rs diff --git a/crates/lsp-profiler-helper/src/main.rs b/crates/lsp-profiler-python/src/bin/lsp-profiler-helper/main.rs similarity index 100% rename from crates/lsp-profiler-helper/src/main.rs rename to crates/lsp-profiler-python/src/bin/lsp-profiler-helper/main.rs diff --git a/crates/lsp-profiler-helper/tests/version_tests.rs b/crates/lsp-profiler-python/tests/helper_version.rs similarity index 100% rename from crates/lsp-profiler-helper/tests/version_tests.rs rename to crates/lsp-profiler-python/tests/helper_version.rs diff --git a/crates/lsp-profiler-python/tests/profiler_helper_socket.rs b/crates/lsp-profiler-python/tests/profiler_helper_socket.rs index 4d619ad..2d634c4 100644 --- a/crates/lsp-profiler-python/tests/profiler_helper_socket.rs +++ b/crates/lsp-profiler-python/tests/profiler_helper_socket.rs @@ -79,7 +79,7 @@ fn helper_binary_path() -> PathBuf { // Fallback for `cargo test -p basilisk-lsp` without a prior workspace build. let status = Command::new(env!("CARGO")) - .args(["build", "-p", "lsp-profiler-helper"]) + .args(["build", "-p", "lsp-profiler-python", "--bin", "lsp-profiler-helper"]) .status() .expect("run cargo build for helper"); assert!(status.success(), "failed to build lsp-profiler-helper"); diff --git a/crates/lsp-profiler/Cargo.toml b/crates/lsp-profiler/Cargo.toml index c4a611c..b711405 100644 --- a/crates/lsp-profiler/Cargo.toml +++ b/crates/lsp-profiler/Cargo.toml @@ -9,6 +9,11 @@ repository.workspace = true readme.workspace = true keywords = ["profiler", "lsp", "flamegraph", "memory", "language-server"] categories = ["development-tools::profiling"] +# Lean tarball: ship sources + the release-staged license only. LICENSE-MIT is +# gitignored (staged at publish time), so `include` forces it into the package. +# `src/**/*` also carries the generated wire model, which build.rs cannot +# regenerate outside the workspace. [PROF-WIRE-PACKAGED] +include = ["src/**/*", "build.rs", "LICENSE-MIT"] build = "build.rs" # The profiler's own lint set, stricter than the lspkit workspace's diff --git a/crates/lsp-profiler/build.rs b/crates/lsp-profiler/build.rs index 586da9a..8c0f27d 100644 --- a/crates/lsp-profiler/build.rs +++ b/crates/lsp-profiler/build.rs @@ -2,25 +2,49 @@ //! `scripts/typediagram/generate.mjs` before rustc runs. The generated file //! is gitignored, so this script is the only path that produces it on a //! fresh checkout. Implements [PROF-WIRE-GEN]. +//! +//! A published crates.io tarball has no workspace around it, so the generator +//! is absent. The release generates the model before `cargo publish` and the +//! crate's `include` ships it; in that packaged layout this script only checks +//! the shipped model is present. [PROF-WIRE-PACKAGED] use std::path::{Path, PathBuf}; use std::process::Command; +/// The generator entry point, relative to the workspace root. +const GENERATOR: &str = "scripts/typediagram/generate.mjs"; + +/// The generated wire model, relative to this crate's manifest directory. +const GENERATED: &str = "src/wire_generated.rs"; + +/// Every generator input; a change to any of them reruns this script. +const INPUTS: [&str; 6] = [ + "docs/models/profiler.td", + "docs/models/profiler-methods.json", + GENERATOR, + "scripts/typediagram/rust-emit.mjs", + "scripts/typediagram/ts-emit.mjs", + "scripts/typediagram/td-docs.mjs", +]; + fn main() -> Result<(), String> { - let root = workspace_root()?; - for input in [ - "docs/models/profiler.td", - "docs/models/profiler-methods.json", - "scripts/typediagram/generate.mjs", - "scripts/typediagram/rust-emit.mjs", - "scripts/typediagram/ts-emit.mjs", - "scripts/typediagram/td-docs.mjs", - ] { + let manifest = manifest_dir()?; + let root = workspace_root(&manifest)?; + if root.join(GENERATOR).is_file() { + generate(&root) + } else { + packaged_model(&manifest) + } +} + +/// Workspace build: run the typeDiagram generator. +fn generate(root: &Path) -> Result<(), String> { + for input in INPUTS { println!("cargo:rerun-if-changed={}", root.join(input).display()); } let status = Command::new("node") - .arg(root.join("scripts/typediagram/generate.mjs")) - .current_dir(&root) + .arg(root.join(GENERATOR)) + .current_dir(root) .status() .map_err(|error| format!("typediagram-gen: cannot spawn node ({error}); install Node.js >= 20 and `npm i -g typediagram`"))?; if status.success() { @@ -32,11 +56,30 @@ fn main() -> Result<(), String> { } } +/// Packaged build: the published tarball MUST carry the generated model. +fn packaged_model(manifest: &Path) -> Result<(), String> { + let model = manifest.join(GENERATED); + println!("cargo:rerun-if-changed={}", model.display()); + if model.is_file() { + Ok(()) + } else { + Err(format!( + "lsp-profiler: no workspace generator at {GENERATOR} and no packaged {GENERATED}; \ + build inside the lspkit workspace or from a published crate" + )) + } +} + +/// This crate's manifest directory, set by cargo for every build script. +fn manifest_dir() -> Result { + std::env::var_os("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .ok_or_else(|| "CARGO_MANIFEST_DIR must be set by cargo".to_owned()) +} + /// The workspace root: two parents above this crate's manifest directory. -fn workspace_root() -> Result { - let manifest = std::env::var_os("CARGO_MANIFEST_DIR") - .ok_or_else(|| "CARGO_MANIFEST_DIR must be set by cargo".to_owned())?; - Path::new(&manifest) +fn workspace_root(manifest: &Path) -> Result { + manifest .parent() .and_then(Path::parent) .map(Path::to_path_buf) diff --git a/crates/lspkit-config/Cargo.toml b/crates/lspkit-config/Cargo.toml deleted file mode 100644 index 4208fb1..0000000 --- a/crates/lspkit-config/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "lspkit-config" -description = "Layered TOML configuration loader generic over the consumer's config struct." -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -documentation = "https://docs.rs/lspkit-config" -# Lean tarball + force the release-staged (gitignored) LICENSE-MIT in. [SWR-REL-LICENSE] -include = ["src/**/*", "LICENSE-MIT"] - -[lints] -workspace = true - -[dependencies] -serde.workspace = true -toml = "1.1" -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -tempfile = "3" -serde.workspace = true diff --git a/crates/lspkit-live/Cargo.toml b/crates/lspkit-live/Cargo.toml deleted file mode 100644 index fbbf2e9..0000000 --- a/crates/lspkit-live/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "lspkit-live" -description = "File-watcher, debouncer, and single-flight scheduler that drive an EngineApi session." -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -documentation = "https://docs.rs/lspkit-live" -# Lean tarball + force the release-staged (gitignored) LICENSE-MIT in. [SWR-REL-LICENSE] -include = ["src/**/*", "LICENSE-MIT"] - -[lints] -workspace = true - -[dependencies] -lspkit.workspace = true - -tokio.workspace = true -tokio-util.workspace = true -notify.workspace = true -async-trait.workspace = true -futures-util.workspace = true - -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -tokio = { workspace = true, features = ["test-util", "macros", "rt-multi-thread", "sync", "time"] } -tempfile = "3" -futures-util.workspace = true -serde_json.workspace = true diff --git a/crates/lspkit-mcp/Cargo.toml b/crates/lspkit-mcp/Cargo.toml deleted file mode 100644 index aa7dc4b..0000000 --- a/crates/lspkit-mcp/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "lspkit-mcp" -description = "MCP adapter that mounts an EngineApi implementation as tools, resources, and prompts." -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -documentation = "https://docs.rs/lspkit-mcp" -# Lean tarball + force the release-staged (gitignored) LICENSE-MIT in. [SWR-REL-LICENSE] -include = ["src/**/*", "LICENSE-MIT"] - -[lints] -workspace = true - -[dependencies] -lspkit.workspace = true - -tokio.workspace = true -async-trait.workspace = true -serde.workspace = true -serde_json.workspace = true -rmcp.workspace = true - -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -async-trait.workspace = true -futures-core.workspace = true -futures-util.workspace = true -tokio-util.workspace = true -serde_json.workspace = true diff --git a/crates/lspkit-server/Cargo.toml b/crates/lspkit-server/Cargo.toml index f95bb62..3c5e518 100644 --- a/crates/lspkit-server/Cargo.toml +++ b/crates/lspkit-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lspkit-server" -description = "Hand-rolled JSON-RPC LSP server scaffolding: Content-Length framing, dispatcher, capability builder, URI helpers, diagnostics fan-out, progress, cancellation." +description = "LSP server runtime on top of lspkit: Content-Length JSON-RPC framing, dispatcher, capabilities, diagnostics fan-out, progress, cancellation, a file-watching analysis scheduler and an optional MCP adapter." version.workspace = true edition.workspace = true rust-version.workspace = true @@ -14,40 +14,42 @@ documentation = "https://docs.rs/lspkit-server" # The example's support modules ship beside its entrypoint. [SWR-REL-EXAMPLE] include = ["src/**/*", "examples/**/*.rs", "LICENSE-MIT"] +# The L1 toolkit crate: depends only on `lspkit`. [CRATE-LAYOUT-LAYERS] [lints] workspace = true [dependencies] -salsa = { version = "0.28.5", optional = true } lspkit.workspace = true -lspkit-vfs.workspace = true -tokio.workspace = true -tokio-util.workspace = true async-trait.workspace = true futures-util.workspace = true - +lsp-types.workspace = true +notify.workspace = true serde.workspace = true serde_json.workspace = true -lsp-types.workspace = true - thiserror.workspace = true +tokio.workspace = true +tokio-util.workspace = true tracing.workspace = true +# Optional: the MCP SDK stays out of LSP-only consumers. [CRATE-LAYOUT-FEATURES] +rmcp = { workspace = true, optional = true } +salsa = { version = "0.28.5", optional = true } + [features] +mcp = ["dep:rmcp"] pull = ["dep:salsa"] [dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "io-util", "time", "io-std", "process"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "io-util", "time", "io-std", "process", "test-util"] } +futures-core.workspace = true futures-util.workspace = true serde_json.workspace = true async-trait.workspace = true -tempfile = "3" -lspkit-live.workspace = true -lspkit-sidecar.workspace = true +tempfile.workspace = true tracing-subscriber.workspace = true -# Real reference pull-diagnostics server exercised by tests/reference_e2e.rs +# Real reference pull-diagnostics server exercised by tests/diagnostic_reference.rs # and its sibling suites; it needs the pull store, so a default-feature # consumer build skips it instead of failing. [LSP-PULL-SALSA] [[example]] diff --git a/crates/lspkit-server/examples/diagnostic-reference/analysis.rs b/crates/lspkit-server/examples/diagnostic-reference/analysis.rs index 9245155..7f0284d 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/analysis.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/analysis.rs @@ -9,8 +9,8 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use lsp_types::{Diagnostic, DiagnosticSeverity, InitializeParams, Position, Range}; -use lspkit_live::watcher::CoalescedWatcher; -use lspkit_live::{FileWatcher, ScheduleConfig}; +use lspkit_server::live::watcher::CoalescedWatcher; +use lspkit_server::live::{FileWatcher, ScheduleConfig}; use lspkit_server::pull::{Analysis, AnalysisError}; use lspkit_server::uri_to_path; use serde_json::Value; diff --git a/crates/lspkit-server/examples/diagnostic-reference/backend.rs b/crates/lspkit-server/examples/diagnostic-reference/backend.rs index 697928e..4f9f927 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/backend.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/backend.rs @@ -1,12 +1,12 @@ //! `--backend` mode: a real out-of-process backend speaking the sidecar wire //! protocol — 4-byte little-endian length-prefixed JSON frames over OS stdio -//! — for `lspkit-sidecar`'s `tests/inbound_routing.rs`. Requests are natural +//! — for `lspkit`'s `tests/sidecar_inbound_routing.rs`. Requests are natural //! backend calls; diagnostics come from real `serde_json` parses of real //! files ([LSP-PULL-SALSA]). use std::process::ExitCode; -use lspkit_sidecar::{TransportError, DEFAULT_MAX_FRAME}; +use lspkit::sidecar::{TransportError, DEFAULT_MAX_FRAME}; use serde_json::{json, Value}; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -53,7 +53,7 @@ async fn next_frame( if reader.fill_buf().await?.is_empty() { return Ok(None); } - lspkit_sidecar::read_frame(reader, DEFAULT_MAX_FRAME) + lspkit::sidecar::read_frame(reader, DEFAULT_MAX_FRAME) .await .map(Some) } @@ -64,7 +64,7 @@ async fn write_envelope( envelope: &Value, ) -> Result<(), BackendError> { let body = serde_json::to_vec(envelope)?; - lspkit_sidecar::write_frame(writer, &body).await?; + lspkit::sidecar::write_frame(writer, &body).await?; Ok(()) } diff --git a/crates/lspkit-server/examples/diagnostic-reference/client.rs b/crates/lspkit-server/examples/diagnostic-reference/client.rs index 49a111c..2200756 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/client.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/client.rs @@ -3,7 +3,7 @@ use std::process::{ExitCode, ExitStatus, Stdio}; -use lspkit_sidecar::{inbound, write_frame, TransportError, DEFAULT_MAX_FRAME}; +use lspkit::sidecar::{inbound, write_frame, TransportError, DEFAULT_MAX_FRAME}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, ChildStdin, Command}; use tokio::task::{JoinError, JoinHandle}; @@ -30,7 +30,7 @@ pub(super) enum ClientError { Limit(#[from] std::num::ParseIntError), /// The native filesystem watcher failed to start or run. #[error(transparent)] - Watch(#[from] lspkit_live::watcher::WatcherError), + Watch(#[from] lspkit_server::live::watcher::WatcherError), /// A required child-process pipe was unavailable. #[error("backend {0} pipe was unavailable")] Pipe(&'static str), diff --git a/crates/lspkit-server/examples/diagnostic-reference/client_wire.rs b/crates/lspkit-server/examples/diagnostic-reference/client_wire.rs index a188cba..835a70c 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/client_wire.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/client_wire.rs @@ -1,7 +1,7 @@ //! Typed backend envelopes and rendered consumer events. [LSP-PULL-TRANSPORT] +use lspkit::sidecar::inbound::Inbound; use lspkit_server::RequestId; -use lspkit_sidecar::inbound::Inbound; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use tokio::io::AsyncWriteExt; diff --git a/crates/lspkit-server/examples/diagnostic-reference/main.rs b/crates/lspkit-server/examples/diagnostic-reference/main.rs index 5e17eaa..53a5251 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/main.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/main.rs @@ -11,7 +11,7 @@ //! //! `--backend` mode is a sidecar-framed out-of-process backend (4-byte //! little-endian length-prefixed JSON over OS stdio) driven by -//! `lspkit-sidecar`'s `tests/inbound_routing.rs`. +//! `lspkit`'s `tests/sidecar_inbound_routing.rs`. mod analysis; mod backend; diff --git a/crates/lspkit-server/examples/diagnostic-reference/receive.rs b/crates/lspkit-server/examples/diagnostic-reference/receive.rs index 6a19a15..22a2158 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/receive.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/receive.rs @@ -3,7 +3,7 @@ use crate::client::{self, ClientError}; use crate::client_wire::{self, Closed}; -use lspkit_sidecar::{inbound, DEFAULT_MAX_FRAME}; +use lspkit::sidecar::{inbound, DEFAULT_MAX_FRAME}; use std::process::ExitCode; /// Open the caller's capture as a real native asynchronous file. diff --git a/crates/lspkit-server/examples/diagnostic-reference/scan.rs b/crates/lspkit-server/examples/diagnostic-reference/scan.rs index 48cba8a..d4761c1 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/scan.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/scan.rs @@ -127,7 +127,7 @@ async fn scan_in_background( cancel: &CancellationToken, ) -> Result<(), ScanError> { let computation = start_document(server, params, gate, uri); - let result = lspkit_sidecar::cancellation::until_cancelled(computation, Some(cancel)) + let result = lspkit::sidecar::cancellation::until_cancelled(computation, Some(cancel)) .await .map_err(|cancelled| { ScanError::Request(cancelled_error().with_data(json!(cancelled.to_string()))) diff --git a/crates/lspkit-server/examples/diagnostic-reference/watch.rs b/crates/lspkit-server/examples/diagnostic-reference/watch.rs index 4fcd133..5ccb034 100644 --- a/crates/lspkit-server/examples/diagnostic-reference/watch.rs +++ b/crates/lspkit-server/examples/diagnostic-reference/watch.rs @@ -2,8 +2,8 @@ use crate::client::{self, ClientError}; use crate::client_wire; -use lspkit_live::watcher::{ChangeEvent, CoalescedWatcher}; -use lspkit_live::{FileWatcher, ScheduleConfig}; +use lspkit_server::live::watcher::{ChangeEvent, CoalescedWatcher}; +use lspkit_server::live::{FileWatcher, ScheduleConfig}; use serde_json::{json, Value}; use std::path::PathBuf; diff --git a/crates/lspkit-server/src/capabilities.rs b/crates/lspkit-server/src/capabilities.rs index 8ea83fa..9cfd497 100644 --- a/crates/lspkit-server/src/capabilities.rs +++ b/crates/lspkit-server/src/capabilities.rs @@ -5,7 +5,7 @@ use std::collections::BTreeSet; -use lspkit_vfs::PositionEncoding; +use lspkit::vfs::PositionEncoding; /// Decode standard initialize capabilities despite lsp-types 0.97's singular field. /// diff --git a/crates/lspkit-server/src/diagnostics.rs b/crates/lspkit-server/src/diagnostics.rs index aa70b4d..0f917d0 100644 --- a/crates/lspkit-server/src/diagnostics.rs +++ b/crates/lspkit-server/src/diagnostics.rs @@ -7,8 +7,8 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; +use lspkit::vfs::DocumentUri; use lspkit::Generation; -use lspkit_vfs::DocumentUri; use crate::uri::path_to_uri; diff --git a/crates/lspkit-server/src/lib.rs b/crates/lspkit-server/src/lib.rs index cce43fd..00646de 100644 --- a/crates/lspkit-server/src/lib.rs +++ b/crates/lspkit-server/src/lib.rs @@ -9,14 +9,20 @@ //! - [`diagnostics::DiagnosticsBus`] for fan-out to LSP and MCP sinks. //! - [`progress::ProgressSink`] wrapping `window/workDoneProgress`. //! - Multi-root [`workspace::Workspace`] tracking. +//! - [`live`] file watching, debouncing and analysis scheduling. +//! - `mcp` — an MCP adapter for the same engine (`mcp` feature). //! -//! All routing happens against an [`lspkit::EngineApi`] + [`lspkit_vfs::Vfs`] -//! pair the consumer supplies. +//! All routing happens against an [`lspkit::EngineApi`] + [`lspkit::vfs::Vfs`] +//! pair the consumer supplies. This is the L1 toolkit crate and depends only +//! on `lspkit` ([CRATE-LAYOUT-LAYERS]). pub mod capabilities; pub mod diagnostics; pub mod dispatch; pub mod jsonrpc; +pub mod live; +#[cfg(feature = "mcp")] +pub mod mcp; pub mod progress; #[cfg(feature = "pull")] pub mod pull; diff --git a/crates/lspkit-live/src/lib.rs b/crates/lspkit-server/src/live.rs similarity index 52% rename from crates/lspkit-live/src/lib.rs rename to crates/lspkit-server/src/live.rs index c1b5187..9994fe0 100644 --- a/crates/lspkit-live/src/lib.rs +++ b/crates/lspkit-server/src/live.rs @@ -1,4 +1,4 @@ -//! `lspkit-live` — file-watcher, debouncer, and session lifecycle. +//! `lspkit_server::live` — file-watcher, debouncer, and session lifecycle. //! //! Produces values that an [`lspkit::EngineApi`] implementation can build on: //! a coarse change stream from [`watcher::FileWatcher`], coalesced by @@ -9,6 +9,6 @@ pub mod scheduler; pub mod session; pub mod watcher; -pub use crate::scheduler::{spawn as spawn_scheduler, ScheduleConfig, Trigger}; -pub use crate::session::{Analyzer, AnalyzerError, AnalyzerResult, Session}; -pub use crate::watcher::{ChangeEvent, FileWatcher, WatcherError}; +pub use crate::live::scheduler::{spawn as spawn_scheduler, ScheduleConfig, Trigger}; +pub use crate::live::session::{Analyzer, AnalyzerError, AnalyzerResult, Session}; +pub use crate::live::watcher::{ChangeEvent, FileWatcher, WatcherError}; diff --git a/crates/lspkit-live/src/scheduler.rs b/crates/lspkit-server/src/live/scheduler.rs similarity index 98% rename from crates/lspkit-live/src/scheduler.rs rename to crates/lspkit-server/src/live/scheduler.rs index 4d07d9b..e230e9b 100644 --- a/crates/lspkit-live/src/scheduler.rs +++ b/crates/lspkit-server/src/live/scheduler.rs @@ -11,7 +11,7 @@ use std::time::Duration; use tokio::sync::mpsc; use tokio::time; -use crate::watcher::ChangeEvent; +use crate::live::watcher::ChangeEvent; /// Scheduler configuration. #[non_exhaustive] diff --git a/crates/lspkit-live/src/session.rs b/crates/lspkit-server/src/live/session.rs similarity index 96% rename from crates/lspkit-live/src/session.rs rename to crates/lspkit-server/src/live/session.rs index a2a1c9d..878b8ba 100644 --- a/crates/lspkit-live/src/session.rs +++ b/crates/lspkit-server/src/live/session.rs @@ -1,6 +1,6 @@ //! Session lifecycle. //! -//! Wires a [`crate::watcher::FileWatcher`] to a [`crate::scheduler`] and an +//! Wires a [`crate::live::watcher::FileWatcher`] to a [`crate::live::scheduler`] and an //! analyzer callback. The session owns the generation counter that //! [`lspkit::EngineApi`] implementations advance on every successful pass. @@ -11,7 +11,7 @@ use async_trait::async_trait; use lspkit::{Cause, Generation, GenerationEvent}; use tokio::sync::broadcast; -use crate::scheduler::Trigger; +use crate::live::scheduler::Trigger; /// Result of a single analysis pass. pub type AnalyzerResult = Result<(), AnalyzerError>; diff --git a/crates/lspkit-live/src/watcher.rs b/crates/lspkit-server/src/live/watcher.rs similarity index 95% rename from crates/lspkit-live/src/watcher.rs rename to crates/lspkit-server/src/live/watcher.rs index 944f79b..d89989d 100644 --- a/crates/lspkit-live/src/watcher.rs +++ b/crates/lspkit-server/src/live/watcher.rs @@ -48,14 +48,14 @@ pub struct FileWatcher { #[derive(Debug)] pub struct CoalescedWatcher { /// Yields batches after the shared scheduler's quiet or capped window. - triggers: mpsc::UnboundedReceiver, + triggers: mpsc::UnboundedReceiver, /// Owns the async pump so dropping this value stops reading native events. reader: tokio::task::JoinHandle<()>, } impl CoalescedWatcher { /// Wait for the next coalesced input batch. - pub async fn next(&mut self) -> Option { + pub async fn next(&mut self) -> Option { self.triggers.recv().await } } @@ -69,7 +69,7 @@ impl Drop for CoalescedWatcher { impl FileWatcher { /// Feed the canonical shared debouncer while retaining native watcher ownership. #[must_use] - pub fn coalesced(mut self, config: crate::scheduler::ScheduleConfig) -> CoalescedWatcher { + pub fn coalesced(mut self, config: crate::live::scheduler::ScheduleConfig) -> CoalescedWatcher { let (sender, receiver) = mpsc::unbounded_channel(); let reader = tokio::spawn(async move { while let Some(event) = self.next().await { @@ -79,7 +79,7 @@ impl FileWatcher { } }); CoalescedWatcher { - triggers: crate::scheduler::spawn(receiver, config), + triggers: crate::live::scheduler::spawn(receiver, config), reader, } } diff --git a/crates/lspkit-mcp/src/lib.rs b/crates/lspkit-server/src/mcp.rs similarity index 54% rename from crates/lspkit-mcp/src/lib.rs rename to crates/lspkit-server/src/mcp.rs index 7614d46..df2bc2d 100644 --- a/crates/lspkit-mcp/src/lib.rs +++ b/crates/lspkit-server/src/mcp.rs @@ -1,4 +1,4 @@ -//! `lspkit-mcp` — mount an [`lspkit::EngineApi`] implementation as an MCP server. +//! `lspkit_server::mcp` — mount an [`lspkit::EngineApi`] implementation as an MCP server. //! //! Wraps the official Rust MCP SDK behind a newtype wall so consumers do not //! depend on the SDK's public API in their own public types. Tools, resources, @@ -8,5 +8,5 @@ pub mod adapter; pub mod tools; -pub use crate::adapter::{Adapter, AdapterError, ToolHandler, ToolInvocation}; -pub use crate::tools::{ToolDescription, ToolRegistry}; +pub use crate::mcp::adapter::{Adapter, AdapterError, ToolHandler, ToolInvocation}; +pub use crate::mcp::tools::{ToolDescription, ToolRegistry}; diff --git a/crates/lspkit-mcp/src/adapter.rs b/crates/lspkit-server/src/mcp/adapter.rs similarity index 98% rename from crates/lspkit-mcp/src/adapter.rs rename to crates/lspkit-server/src/mcp/adapter.rs index aead0d9..f55f3bc 100644 --- a/crates/lspkit-mcp/src/adapter.rs +++ b/crates/lspkit-server/src/mcp/adapter.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use lspkit::EngineApi; use serde_json::Value; -use crate::tools::{ToolDescription, ToolRegistry}; +use crate::mcp::tools::{ToolDescription, ToolRegistry}; /// Result of invoking a tool. #[non_exhaustive] diff --git a/crates/lspkit-mcp/src/tools.rs b/crates/lspkit-server/src/mcp/tools.rs similarity index 100% rename from crates/lspkit-mcp/src/tools.rs rename to crates/lspkit-server/src/mcp/tools.rs diff --git a/crates/lspkit-server/tests/capabilities.rs b/crates/lspkit-server/tests/capabilities.rs index b13b884..07f4197 100644 --- a/crates/lspkit-server/tests/capabilities.rs +++ b/crates/lspkit-server/tests/capabilities.rs @@ -1,7 +1,7 @@ //! End-to-end tests for the [`lspkit_server::capabilities`] builder. +use lspkit::vfs::PositionEncoding; use lspkit_server::{Capabilities, Feature, TextSync}; -use lspkit_vfs::PositionEncoding; #[test] fn new_has_default_encoding_and_incremental_sync() { diff --git a/crates/lspkit-server/tests/cli_sessions/unrepresentable.rs b/crates/lspkit-server/tests/cli_sessions/unrepresentable.rs index eed6819..1c32124 100644 --- a/crates/lspkit-server/tests/cli_sessions/unrepresentable.rs +++ b/crates/lspkit-server/tests/cli_sessions/unrepresentable.rs @@ -3,14 +3,19 @@ use std::ffi::OsString; use std::os::unix::ffi::OsStringExt; +use std::path::{Path, PathBuf}; +use super::common::bulk_workspace; +#[cfg(not(target_vendor = "apple"))] use super::common::{ - bulk_workspace, clean_exit, error_of, identity, initialize_session, result_of, uri_for, - ReferenceClient, + clean_exit, error_of, identity, initialize_session, result_of, uri_for, ReferenceClient, }; +#[cfg(not(target_vendor = "apple"))] use lspkit_server::{JsonRpcError, Message}; +#[cfg(not(target_vendor = "apple"))] use serde_json::{json, Value}; +#[cfg(not(target_vendor = "apple"))] /// Omitting the source would fabricate a successful, incomplete workspace report. fn assert_scan_failure(response: &Message, error: &JsonRpcError, work: &Value) { assert!( @@ -31,6 +36,7 @@ fn assert_scan_failure(response: &Message, error: &JsonRpcError, work: &Value) { ); } +#[cfg(not(target_vendor = "apple"))] /// Native recovery must return the complete remaining source under a newer epoch. fn assert_scan_recovered(response: &Message, expected_uri: &str, before: u64) { assert!( @@ -55,6 +61,7 @@ fn assert_scan_recovered(response: &Message, expected_uri: &str, before: u64) { } } +#[cfg(not(target_vendor = "apple"))] /// The one remaining source is full, clean, and at a newer epoch. fn assert_remaining(remaining: &Value, expected_uri: &str, before: u64) { assert_eq!(remaining.get("uri").cloned(), Some(json!(expected_uri))); @@ -72,13 +79,39 @@ fn assert_remaining(remaining: &Value, expected_uri: &str, before: u64) { ); } -/// The actual native filename is legal on Unix and cannot be represented by LSP. +/// `EILSEQ`: Apple filesystems refuse a name that is not valid UTF-8. +#[cfg(target_vendor = "apple")] +const ILLEGAL_BYTE_SEQUENCE: i32 = 92; + +/// A native source filename that is not UTF-8, so no file URI can name it. +fn unrepresentable_source(root: &Path) -> PathBuf { + root.join(OsString::from_vec(b"invalid-\xff.json".to_vec())) +} + +/// APFS and HFS+ store names as UTF-8, so on Apple platforms an unrepresentable +/// source can never enter the workspace the scan enumerates. +#[cfg(target_vendor = "apple")] +#[test] +fn apple_filesystems_refuse_an_unrepresentable_source_name() { + let workspace = bulk_workspace(1).expect("scan workspace fixture"); + let source = unrepresentable_source(workspace.path()); + let error = std::fs::write(&source, b"{\"broken\":") + .expect_err("an Apple filesystem refuses a non-UTF-8 name"); + assert_eq!(error.raw_os_error(), Some(ILLEGAL_BYTE_SEQUENCE)); + assert!(!source.exists(), "the refused source never exists"); + let names: Vec = std::fs::read_dir(workspace.path()) + .expect("list scan workspace") + .map(|entry| entry.expect("workspace entry").file_name()) + .collect(); + assert_eq!(names, vec![OsString::from("scan-0000.json")]); +} + +/// The actual native filename is legal on Linux and cannot be represented by LSP. +#[cfg(not(target_vendor = "apple"))] #[tokio::test] async fn unrepresentable_source_is_an_honest_scan_error_and_native_removal_recovers() { let workspace = bulk_workspace(1).expect("scan workspace fixture"); - let source = workspace - .path() - .join(OsString::from_vec(b"invalid-\xff.json".to_vec())); + let source = unrepresentable_source(workspace.path()); std::fs::write(&source, b"{\"broken\":").expect("write actual unrepresentable source"); let mut client = ReferenceClient::spawn().expect("spawn reference server"); initialize_session!(client, workspace.path()); diff --git a/crates/lspkit-server/tests/diagnostics.rs b/crates/lspkit-server/tests/diagnostics.rs index 85d3e81..fbd8028 100644 --- a/crates/lspkit-server/tests/diagnostics.rs +++ b/crates/lspkit-server/tests/diagnostics.rs @@ -3,12 +3,12 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; +use lspkit::vfs::DocumentUri; use lspkit::Generation; use lspkit_server::{ diagnostics::document_uri_from_path, Diagnostic, DiagnosticsBatch, DiagnosticsBus, DiagnosticsSink, Severity, }; -use lspkit_vfs::DocumentUri; /// Captures every batch a bus publishes, for whole-history assertions. struct Recorder { diff --git a/crates/lspkit-live/tests/common/mod.rs b/crates/lspkit-server/tests/live_common/mod.rs similarity index 100% rename from crates/lspkit-live/tests/common/mod.rs rename to crates/lspkit-server/tests/live_common/mod.rs diff --git a/crates/lspkit-live/tests/scheduler_tests.rs b/crates/lspkit-server/tests/live_scheduler.rs similarity index 98% rename from crates/lspkit-live/tests/scheduler_tests.rs rename to crates/lspkit-server/tests/live_scheduler.rs index 5684c3b..7497e02 100644 --- a/crates/lspkit-live/tests/scheduler_tests.rs +++ b/crates/lspkit-server/tests/live_scheduler.rs @@ -8,7 +8,7 @@ use std::path::PathBuf; use std::time::Duration; -use lspkit_live::{spawn_scheduler, ChangeEvent, ScheduleConfig, Trigger}; +use lspkit_server::live::{spawn_scheduler, ChangeEvent, ScheduleConfig, Trigger}; use tokio::sync::mpsc; use tokio::time; diff --git a/crates/lspkit-live/tests/session_tests.rs b/crates/lspkit-server/tests/live_session.rs similarity index 99% rename from crates/lspkit-live/tests/session_tests.rs rename to crates/lspkit-server/tests/live_session.rs index 87e583d..7967582 100644 --- a/crates/lspkit-live/tests/session_tests.rs +++ b/crates/lspkit-server/tests/live_session.rs @@ -12,7 +12,7 @@ use std::time::Duration; use async_trait::async_trait; use lspkit::{Cause, Generation}; -use lspkit_live::{ +use lspkit_server::live::{ spawn_scheduler, Analyzer, AnalyzerError, AnalyzerResult, ChangeEvent, ScheduleConfig, Session, Trigger, }; diff --git a/crates/lspkit-live/tests/watcher_tests.rs b/crates/lspkit-server/tests/live_watcher.rs similarity index 98% rename from crates/lspkit-live/tests/watcher_tests.rs rename to crates/lspkit-server/tests/live_watcher.rs index 8939721..3af4efd 100644 --- a/crates/lspkit-live/tests/watcher_tests.rs +++ b/crates/lspkit-server/tests/live_watcher.rs @@ -13,7 +13,7 @@ use std::path::PathBuf; use std::time::Duration; -use lspkit_live::{ChangeEvent, FileWatcher, WatcherError}; +use lspkit_server::live::{ChangeEvent, FileWatcher, WatcherError}; use tempfile::TempDir; use tokio::time::timeout; diff --git a/crates/lspkit-live/tests/watcher_coalesced_tests.rs b/crates/lspkit-server/tests/live_watcher_coalesced.rs similarity index 99% rename from crates/lspkit-live/tests/watcher_coalesced_tests.rs rename to crates/lspkit-server/tests/live_watcher_coalesced.rs index a362753..96d8664 100644 --- a/crates/lspkit-live/tests/watcher_coalesced_tests.rs +++ b/crates/lspkit-server/tests/live_watcher_coalesced.rs @@ -7,6 +7,7 @@ use tempfile::TempDir; use common::at; use native_events::NativeEvents; +#[path = "live_common/mod.rs"] mod common; #[path = "../../../test-support/native_events.rs"] #[expect( diff --git a/crates/lspkit-live/tests/watcher_depth_tests.rs b/crates/lspkit-server/tests/live_watcher_depth.rs similarity index 99% rename from crates/lspkit-live/tests/watcher_depth_tests.rs rename to crates/lspkit-server/tests/live_watcher_depth.rs index 18357a3..c9ea177 100644 --- a/crates/lspkit-live/tests/watcher_depth_tests.rs +++ b/crates/lspkit-server/tests/live_watcher_depth.rs @@ -6,6 +6,7 @@ use tempfile::TempDir; use common::at; use native_events::NativeEvents; +#[path = "live_common/mod.rs"] mod common; #[path = "../../../test-support/native_events.rs"] #[expect( diff --git a/crates/lspkit-mcp/tests/adapter.rs b/crates/lspkit-server/tests/mcp_adapter.rs similarity index 97% rename from crates/lspkit-mcp/tests/adapter.rs rename to crates/lspkit-server/tests/mcp_adapter.rs index fc55a5f..092fcec 100644 --- a/crates/lspkit-mcp/tests/adapter.rs +++ b/crates/lspkit-server/tests/mcp_adapter.rs @@ -1,5 +1,8 @@ -//! Coverage for [`lspkit_mcp::Adapter`] construction, dispatch, and errors. +//! Coverage for [`lspkit_server::mcp::Adapter`] construction, dispatch, and errors. +#![cfg(feature = "mcp")] + +#[path = "mcp_common/mod.rs"] mod common; use std::sync::Arc; @@ -7,7 +10,7 @@ use std::sync::Arc; use async_trait::async_trait; use common::{FakeEngine, FakeError}; use lspkit::EngineApi; -use lspkit_mcp::{Adapter, AdapterError, ToolDescription, ToolHandler}; +use lspkit_server::mcp::{Adapter, AdapterError, ToolDescription, ToolHandler}; use serde_json::{json, Value}; use tokio_util::sync::CancellationToken; diff --git a/crates/lspkit-mcp/tests/common/mod.rs b/crates/lspkit-server/tests/mcp_common/mod.rs similarity index 100% rename from crates/lspkit-mcp/tests/common/mod.rs rename to crates/lspkit-server/tests/mcp_common/mod.rs diff --git a/crates/lspkit-mcp/tests/tools.rs b/crates/lspkit-server/tests/mcp_tools.rs similarity index 96% rename from crates/lspkit-mcp/tests/tools.rs rename to crates/lspkit-server/tests/mcp_tools.rs index 09f2ef4..1d64deb 100644 --- a/crates/lspkit-mcp/tests/tools.rs +++ b/crates/lspkit-server/tests/mcp_tools.rs @@ -1,6 +1,8 @@ //! Coverage for `ToolDescription` and `ToolRegistry`. -use lspkit_mcp::{ToolDescription, ToolRegistry}; +#![cfg(feature = "mcp")] + +use lspkit_server::mcp::{ToolDescription, ToolRegistry}; use serde_json::json; fn make_tool(name: &str) -> ToolDescription { diff --git a/crates/lspkit-sidecar/Cargo.toml b/crates/lspkit-sidecar/Cargo.toml deleted file mode 100644 index 34fc0b3..0000000 --- a/crates/lspkit-sidecar/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "lspkit-sidecar" -description = "Framed-IPC primitives and lifecycle/health/respawn for out-of-process backends. Pure transport — no backend-specific code." -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -documentation = "https://docs.rs/lspkit-sidecar" -# Lean tarball + force the release-staged (gitignored) LICENSE-MIT in. [SWR-REL-LICENSE] -include = ["src/**/*", "LICENSE-MIT"] - -[lints] -workspace = true - -[dependencies] -tokio.workspace = true -tokio-util.workspace = true -serde.workspace = true -rmp-serde.workspace = true - -thiserror.workspace = true -tracing.workspace = true - -[dev-dependencies] -# The inbound-routing integration drives the real compiled -# `diagnostic-reference --backend` child over OS stdio, so the test runtime -# needs process spawning plus framed IO. -tokio = { workspace = true, features = ["rt", "macros", "process", "io-util"] } -serde_json.workspace = true -tempfile = "3" diff --git a/crates/lspkit-treesitter/Cargo.toml b/crates/lspkit-treesitter/Cargo.toml deleted file mode 100644 index 5cae7fb..0000000 --- a/crates/lspkit-treesitter/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "lspkit-treesitter" -description = "LanguageParser trait + content-hash-keyed parse cache. Bundles no grammars." -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -documentation = "https://docs.rs/lspkit-treesitter" -# Lean tarball + force the release-staged (gitignored) LICENSE-MIT in. [SWR-REL-LICENSE] -include = ["src/**/*", "LICENSE-MIT"] - -[lints] -workspace = true - -[dependencies] -tree-sitter.workspace = true -dashmap.workspace = true -thiserror.workspace = true - -[dev-dependencies] -tree-sitter-rust = "0.24" diff --git a/crates/lspkit-vfs/Cargo.toml b/crates/lspkit-vfs/Cargo.toml deleted file mode 100644 index c319163..0000000 --- a/crates/lspkit-vfs/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "lspkit-vfs" -description = "Concurrent open-document store with version tracking, rope-based incremental edits, and position-encoding negotiation." -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -documentation = "https://docs.rs/lspkit-vfs" -# Lean tarball + force the release-staged (gitignored) LICENSE-MIT in. [SWR-REL-LICENSE] -include = ["src/**/*", "LICENSE-MIT"] - -[lints] -workspace = true - -[dependencies] -dashmap.workspace = true -ropey.workspace = true -thiserror.workspace = true diff --git a/crates/lspkit/Cargo.toml b/crates/lspkit/Cargo.toml index 605d320..9a1ef32 100644 --- a/crates/lspkit/Cargo.toml +++ b/crates/lspkit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lspkit" -description = "Generic Rust infrastructure for building LSP+MCP servers — façade crate exposing the EngineApi contract." +description = "Foundation for building LSP+MCP servers: the EngineApi contract, layered TOML config, a rope-backed open-document store, framed-IPC sidecars and an optional tree-sitter parse cache." version.workspace = true edition.workspace = true rust-version.workspace = true @@ -15,25 +15,40 @@ documentation = "https://docs.rs/lspkit" # [SWR-REL-LICENSE] include = ["src/**/*", "LICENSE-MIT"] +# The L0 toolkit crate: no internal dependencies. [CRATE-LAYOUT-LAYERS] [lints] workspace = true [dependencies] async-trait.workspace = true +dashmap.workspace = true futures-core.workspace = true +rmp-serde.workspace = true +ropey.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true tokio-util.workspace = true +toml.workspace = true +tracing.workspace = true +# Optional: tree-sitter compiles a C runtime, so LSP-only consumers opt in. +# [CRATE-LAYOUT-FEATURES] +tree-sitter = { workspace = true, optional = true } # Optional: feature-gated tracing-subscriber bootstrap. -tracing = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true } -thiserror = { workspace = true, optional = true } [features] default = [] -tracing-setup = ["dep:tracing", "dep:tracing-subscriber", "dep:thiserror"] +tracing-setup = ["dep:tracing-subscriber"] +treesitter = ["dep:tree-sitter"] [dev-dependencies] -tokio = { workspace = true } +# The sidecar inbound-routing integration drives the real compiled +# `diagnostic-reference --backend` child over OS stdio, so the test runtime +# needs process spawning plus framed IO. +tokio = { workspace = true, features = ["rt", "macros", "process", "io-util"] } futures-util.workspace = true -async-trait.workspace = true -tokio-util.workspace = true +serde_json.workspace = true +tempfile.workspace = true +tree-sitter-rust = "0.24" diff --git a/crates/lspkit-config/src/lib.rs b/crates/lspkit/src/config.rs similarity index 69% rename from crates/lspkit-config/src/lib.rs rename to crates/lspkit/src/config.rs index dd67700..a5f1bca 100644 --- a/crates/lspkit-config/src/lib.rs +++ b/crates/lspkit/src/config.rs @@ -1,10 +1,10 @@ -//! `lspkit-config` — layered TOML config loader for LSP/MCP servers. +//! `lspkit::config` — layered TOML config loader for LSP/MCP servers. //! //! Walks up from a workspace root looking for a named config file and //! deserializes it into a consumer-supplied `serde::Deserialize` type. pub mod layered; -pub use crate::layered::{ +pub use crate::config::layered::{ find_ancestor, load_from_ancestor, load_from_path, LoadError, LoadedConfig, }; diff --git a/crates/lspkit-config/src/layered.rs b/crates/lspkit/src/config/layered.rs similarity index 100% rename from crates/lspkit-config/src/layered.rs rename to crates/lspkit/src/config/layered.rs diff --git a/crates/lspkit/src/engine.rs b/crates/lspkit/src/engine.rs index 250bc5d..6788765 100644 --- a/crates/lspkit/src/engine.rs +++ b/crates/lspkit/src/engine.rs @@ -181,7 +181,7 @@ pub type GenerationEventStream = Pin + Se /// The contract every engine implementation satisfies. /// /// One implementation is consumed by both the LSP server scaffolding in -/// `lspkit-server` and the MCP adapter in `lspkit-mcp`. See `spec.md` for the +/// `lspkit-server` and the MCP adapter in `lspkit_server::mcp`. See `spec.md` for the /// full behavioral contract. #[async_trait] pub trait EngineApi: Send + Sync + 'static { diff --git a/crates/lspkit/src/lib.rs b/crates/lspkit/src/lib.rs index c66937a..e107adf 100644 --- a/crates/lspkit/src/lib.rs +++ b/crates/lspkit/src/lib.rs @@ -5,12 +5,25 @@ //! once and vended two ways. See `spec.md` at the workspace root for the full //! behavioral contract. //! +//! This is the L0 toolkit crate ([CRATE-LAYOUT-LAYERS]). Its modules have no +//! internal dependencies: +//! +//! - [`config`] — layered TOML config loader. +//! - [`vfs`] — concurrent open-document store with position-encoding support. +//! - [`sidecar`] — framed-IPC transport and lifecycle for out-of-process backends. +//! - `treesitter` — parser trait and parse cache (`treesitter` feature). +//! //! ``` //! use lspkit::prelude::*; //! ``` +pub mod config; pub mod engine; pub mod prelude; +pub mod sidecar; +#[cfg(feature = "treesitter")] +pub mod treesitter; +pub mod vfs; #[cfg(feature = "tracing-setup")] pub mod tracing_setup; diff --git a/crates/lspkit-sidecar/src/lib.rs b/crates/lspkit/src/sidecar.rs similarity index 62% rename from crates/lspkit-sidecar/src/lib.rs rename to crates/lspkit/src/sidecar.rs index 9bb61c1..7a0ac6e 100644 --- a/crates/lspkit-sidecar/src/lib.rs +++ b/crates/lspkit/src/sidecar.rs @@ -1,4 +1,4 @@ -//! `lspkit-sidecar` — framed-IPC primitives and lifecycle management for +//! `lspkit::sidecar` — framed-IPC primitives and lifecycle management for //! out-of-process backends. //! //! **Pure transport.** This crate has no opinion on the wire format @@ -15,6 +15,6 @@ pub mod inbound; pub mod lifecycle; pub mod transport; -pub use crate::correlator::{CorrelationError, Correlator, RequestId}; -pub use crate::lifecycle::{LifecycleConfig, LifecycleError, Sidecar}; -pub use crate::transport::{read_frame, write_frame, TransportError, DEFAULT_MAX_FRAME}; +pub use crate::sidecar::correlator::{CorrelationError, Correlator, RequestId}; +pub use crate::sidecar::lifecycle::{LifecycleConfig, LifecycleError, Sidecar}; +pub use crate::sidecar::transport::{read_frame, write_frame, TransportError, DEFAULT_MAX_FRAME}; diff --git a/crates/lspkit-sidecar/src/cancellation.rs b/crates/lspkit/src/sidecar/cancellation.rs similarity index 100% rename from crates/lspkit-sidecar/src/cancellation.rs rename to crates/lspkit/src/sidecar/cancellation.rs diff --git a/crates/lspkit-sidecar/src/correlator.rs b/crates/lspkit/src/sidecar/correlator.rs similarity index 100% rename from crates/lspkit-sidecar/src/correlator.rs rename to crates/lspkit/src/sidecar/correlator.rs diff --git a/crates/lspkit-sidecar/src/inbound.rs b/crates/lspkit/src/sidecar/inbound.rs similarity index 98% rename from crates/lspkit-sidecar/src/inbound.rs rename to crates/lspkit/src/sidecar/inbound.rs index 403301a..915817b 100644 --- a/crates/lspkit-sidecar/src/inbound.rs +++ b/crates/lspkit/src/sidecar/inbound.rs @@ -7,7 +7,7 @@ use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::sync::mpsc; use tokio::task::JoinHandle; -use crate::transport::read_frame; +use crate::sidecar::transport::read_frame; /// A single reader task with distinct notification and response channels. #[derive(Debug)] diff --git a/crates/lspkit-sidecar/src/lifecycle.rs b/crates/lspkit/src/sidecar/lifecycle.rs similarity index 100% rename from crates/lspkit-sidecar/src/lifecycle.rs rename to crates/lspkit/src/sidecar/lifecycle.rs diff --git a/crates/lspkit-sidecar/src/transport.rs b/crates/lspkit/src/sidecar/transport.rs similarity index 100% rename from crates/lspkit-sidecar/src/transport.rs rename to crates/lspkit/src/sidecar/transport.rs diff --git a/crates/lspkit-treesitter/src/lib.rs b/crates/lspkit/src/treesitter.rs similarity index 51% rename from crates/lspkit-treesitter/src/lib.rs rename to crates/lspkit/src/treesitter.rs index 341daf1..e4f16f8 100644 --- a/crates/lspkit-treesitter/src/lib.rs +++ b/crates/lspkit/src/treesitter.rs @@ -1,4 +1,4 @@ -//! `lspkit-treesitter` — pluggable tree-sitter parser registry with a +//! `lspkit::treesitter` — pluggable tree-sitter parser registry with a //! content-hash-keyed parse cache. **No grammars are bundled** — consumers //! depend on whichever grammar crates they need and implement //! [`parser::LanguageParser`] for each. @@ -6,5 +6,5 @@ pub mod cache; pub mod parser; -pub use crate::cache::{ContentHash, ParseCache}; -pub use crate::parser::{LanguageParser, ParserError}; +pub use crate::treesitter::cache::{ContentHash, ParseCache}; +pub use crate::treesitter::parser::{LanguageParser, ParserError}; diff --git a/crates/lspkit-treesitter/src/cache.rs b/crates/lspkit/src/treesitter/cache.rs similarity index 98% rename from crates/lspkit-treesitter/src/cache.rs rename to crates/lspkit/src/treesitter/cache.rs index d80954f..b150b05 100644 --- a/crates/lspkit-treesitter/src/cache.rs +++ b/crates/lspkit/src/treesitter/cache.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use dashmap::DashMap; use tree_sitter::{Language, Parser, Tree}; -use crate::parser::ParserError; +use crate::treesitter::parser::ParserError; /// FNV-1a-style content hash. Cheap to compute, sufficient for cache keys. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/lspkit-treesitter/src/parser.rs b/crates/lspkit/src/treesitter/parser.rs similarity index 100% rename from crates/lspkit-treesitter/src/parser.rs rename to crates/lspkit/src/treesitter/parser.rs diff --git a/crates/lspkit-vfs/src/lib.rs b/crates/lspkit/src/vfs.rs similarity index 54% rename from crates/lspkit-vfs/src/lib.rs rename to crates/lspkit/src/vfs.rs index e66c7de..da1b815 100644 --- a/crates/lspkit-vfs/src/lib.rs +++ b/crates/lspkit/src/vfs.rs @@ -1,4 +1,4 @@ -//! `lspkit-vfs` — concurrent open-document store for LSP and analysis tools. +//! `lspkit::vfs` — concurrent open-document store for LSP and analysis tools. //! //! Tracks open documents by URI with LSP version numbers and applies //! incremental edits via a rope buffer. The negotiated [`PositionEncoding`] @@ -9,6 +9,6 @@ pub mod document; pub mod edit; pub mod encoding; -pub use crate::document::{DocumentUri, DocumentVersion, Vfs, VfsError}; -pub use crate::edit::{EditError, Position, Range, TextEdit}; -pub use crate::encoding::{negotiate, PositionEncoding, UnknownEncoding}; +pub use crate::vfs::document::{DocumentUri, DocumentVersion, Vfs, VfsError}; +pub use crate::vfs::edit::{EditError, Position, Range, TextEdit}; +pub use crate::vfs::encoding::{negotiate, PositionEncoding, UnknownEncoding}; diff --git a/crates/lspkit-vfs/src/document.rs b/crates/lspkit/src/vfs/document.rs similarity index 96% rename from crates/lspkit-vfs/src/document.rs rename to crates/lspkit/src/vfs/document.rs index 41847cc..6ef6430 100644 --- a/crates/lspkit-vfs/src/document.rs +++ b/crates/lspkit/src/vfs/document.rs @@ -1,7 +1,7 @@ //! Concurrent open-document store. //! //! Tracks open documents by URI with LSP version numbers. Edits are applied -//! through [`crate::edit`] using the negotiated position encoding. Locks are +//! through [`crate::vfs::edit`] using the negotiated position encoding. Locks are //! always scoped inside synchronous blocks — no guard ever crosses an //! `await` boundary. @@ -10,8 +10,8 @@ use std::sync::Arc; use dashmap::DashMap; use ropey::Rope; -use crate::edit::{self, EditError, TextEdit}; -use crate::encoding::PositionEncoding; +use crate::vfs::edit::{self, EditError, TextEdit}; +use crate::vfs::encoding::PositionEncoding; /// Opaque URI identifying a document. Generic over the wire form so this /// crate does not leak `lsp_types` into its public surface. diff --git a/crates/lspkit-vfs/src/edit.rs b/crates/lspkit/src/vfs/edit.rs similarity index 99% rename from crates/lspkit-vfs/src/edit.rs rename to crates/lspkit/src/vfs/edit.rs index 707b9eb..73f1baf 100644 --- a/crates/lspkit-vfs/src/edit.rs +++ b/crates/lspkit/src/vfs/edit.rs @@ -6,7 +6,7 @@ use ropey::Rope; -use crate::encoding::PositionEncoding; +use crate::vfs::encoding::PositionEncoding; /// A zero-based `(line, character)` position in a document. /// diff --git a/crates/lspkit-vfs/src/encoding.rs b/crates/lspkit/src/vfs/encoding.rs similarity index 100% rename from crates/lspkit-vfs/src/encoding.rs rename to crates/lspkit/src/vfs/encoding.rs diff --git a/crates/lspkit-config/tests/loader.rs b/crates/lspkit/tests/config_loader.rs similarity index 98% rename from crates/lspkit-config/tests/loader.rs rename to crates/lspkit/tests/config_loader.rs index 1fb1e1c..39c1ded 100644 --- a/crates/lspkit-config/tests/loader.rs +++ b/crates/lspkit/tests/config_loader.rs @@ -6,7 +6,7 @@ use std::fs; use std::path::PathBuf; -use lspkit_config::{find_ancestor, load_from_ancestor, load_from_path, LoadError, LoadedConfig}; +use lspkit::config::{find_ancestor, load_from_ancestor, load_from_path, LoadError, LoadedConfig}; use serde::Deserialize; use tempfile::TempDir; diff --git a/crates/lspkit-sidecar/tests/cancellation_session.rs b/crates/lspkit/tests/sidecar_cancellation_session.rs similarity index 97% rename from crates/lspkit-sidecar/tests/cancellation_session.rs rename to crates/lspkit/tests/sidecar_cancellation_session.rs index 152c50a..755eeb8 100644 --- a/crates/lspkit-sidecar/tests/cancellation_session.rs +++ b/crates/lspkit/tests/sidecar_cancellation_session.rs @@ -1,7 +1,7 @@ //! Actual native replies retain their request identity through cancellation. //! [LSP-PULL-CANCELLATION] [LSP-PULL-TRANSPORT] -use lspkit_sidecar::{cancellation, inbound, write_frame, DEFAULT_MAX_FRAME}; +use lspkit::sidecar::{cancellation, inbound, write_frame, DEFAULT_MAX_FRAME}; use serde_json::{json, Value}; use std::path::Path; use tokio::process::{Child, ChildStdin}; @@ -10,6 +10,7 @@ use tokio_util::sync::CancellationToken; use common::{at, entry}; +#[path = "sidecar_common/mod.rs"] mod common; #[path = "../../../test-support/reference_program.rs"] mod reference_program; @@ -221,17 +222,17 @@ async fn ended_real_backend_is_an_error_with_and_without_cancellation() { } } -fn native_watcher(root: &Path) -> TestResult { +fn native_watcher(root: &Path) -> TestResult { let program = reference_program::reference_binary()?; let args = vec![ "--watch".to_owned(), root.display().to_string(), "0".to_owned(), ]; - Ok(lspkit_sidecar::Sidecar::new( + Ok(lspkit::sidecar::Sidecar::new( program.to_str().ok_or("backend path is not UTF8")?, args, - lspkit_sidecar::LifecycleConfig::default(), + lspkit::sidecar::LifecycleConfig::default(), )) } diff --git a/crates/lspkit-sidecar/tests/common/mod.rs b/crates/lspkit/tests/sidecar_common/mod.rs similarity index 100% rename from crates/lspkit-sidecar/tests/common/mod.rs rename to crates/lspkit/tests/sidecar_common/mod.rs diff --git a/crates/lspkit-sidecar/tests/correlator.rs b/crates/lspkit/tests/sidecar_correlator.rs similarity index 98% rename from crates/lspkit-sidecar/tests/correlator.rs rename to crates/lspkit/tests/sidecar_correlator.rs index a94d093..6dc9bdc 100644 --- a/crates/lspkit-sidecar/tests/correlator.rs +++ b/crates/lspkit/tests/sidecar_correlator.rs @@ -3,7 +3,7 @@ //! Drives the public API only — `Correlator::new`, `register`, `resolve`, //! `pending_count`, and `RequestId::get`. -use lspkit_sidecar::{CorrelationError, Correlator, RequestId}; +use lspkit::sidecar::{CorrelationError, Correlator, RequestId}; /// Allocated IDs are monotonic starting at 1. #[tokio::test] diff --git a/crates/lspkit-sidecar/tests/disconnect_session.rs b/crates/lspkit/tests/sidecar_disconnect_session.rs similarity index 99% rename from crates/lspkit-sidecar/tests/disconnect_session.rs rename to crates/lspkit/tests/sidecar_disconnect_session.rs index 63f3653..9a0d2ab 100644 --- a/crates/lspkit-sidecar/tests/disconnect_session.rs +++ b/crates/lspkit/tests/sidecar_disconnect_session.rs @@ -5,6 +5,7 @@ use serde_json::{json, Value}; use common::{at, entry}; +#[path = "sidecar_common/mod.rs"] mod common; #[path = "../../../test-support/frame_events.rs"] #[expect( diff --git a/crates/lspkit-sidecar/tests/inbound_routing.rs b/crates/lspkit/tests/sidecar_inbound_routing.rs similarity index 99% rename from crates/lspkit-sidecar/tests/inbound_routing.rs rename to crates/lspkit/tests/sidecar_inbound_routing.rs index 25a8614..2260fcd 100644 --- a/crates/lspkit-sidecar/tests/inbound_routing.rs +++ b/crates/lspkit/tests/sidecar_inbound_routing.rs @@ -5,6 +5,7 @@ use serde_json::{json, Value}; use common::{at, entry}; +#[path = "sidecar_common/mod.rs"] mod common; #[path = "../../../test-support/frame_events.rs"] #[expect( diff --git a/crates/lspkit-sidecar/tests/lifecycle.rs b/crates/lspkit/tests/sidecar_lifecycle.rs similarity index 98% rename from crates/lspkit-sidecar/tests/lifecycle.rs rename to crates/lspkit/tests/sidecar_lifecycle.rs index dba2ca4..bb7e4f8 100644 --- a/crates/lspkit-sidecar/tests/lifecycle.rs +++ b/crates/lspkit/tests/sidecar_lifecycle.rs @@ -6,7 +6,7 @@ use std::time::Duration; -use lspkit_sidecar::{LifecycleConfig, LifecycleError, Sidecar}; +use lspkit::sidecar::{LifecycleConfig, LifecycleError, Sidecar}; // --- pure-logic cases (no subprocess) ------------------------------------- diff --git a/crates/lspkit-sidecar/tests/transport.rs b/crates/lspkit/tests/sidecar_transport.rs similarity index 98% rename from crates/lspkit-sidecar/tests/transport.rs rename to crates/lspkit/tests/sidecar_transport.rs index 1e5e203..19aeb37 100644 --- a/crates/lspkit-sidecar/tests/transport.rs +++ b/crates/lspkit/tests/sidecar_transport.rs @@ -4,7 +4,7 @@ //! the public `read_frame` / `write_frame` API without reaching into //! internals. -use lspkit_sidecar::{read_frame, write_frame, TransportError, DEFAULT_MAX_FRAME}; +use lspkit::sidecar::{read_frame, write_frame, TransportError, DEFAULT_MAX_FRAME}; use tokio::io::{duplex, AsyncWriteExt}; /// Default-max constant matches the documented 16 MiB ceiling. diff --git a/crates/lspkit-treesitter/tests/cache.rs b/crates/lspkit/tests/treesitter_cache.rs similarity index 98% rename from crates/lspkit-treesitter/tests/cache.rs rename to crates/lspkit/tests/treesitter_cache.rs index 02b9024..a4f5c37 100644 --- a/crates/lspkit-treesitter/tests/cache.rs +++ b/crates/lspkit/tests/treesitter_cache.rs @@ -1,8 +1,10 @@ //! Coarse E2E tests for the parse cache. +#![cfg(feature = "treesitter")] + use std::sync::Arc; -use lspkit_treesitter::{ContentHash, ParseCache}; +use lspkit::treesitter::{ContentHash, ParseCache}; use tree_sitter::Language; fn rust_language() -> Language { diff --git a/crates/lspkit-treesitter/tests/parser.rs b/crates/lspkit/tests/treesitter_parser.rs similarity index 93% rename from crates/lspkit-treesitter/tests/parser.rs rename to crates/lspkit/tests/treesitter_parser.rs index 9dee073..ac6b583 100644 --- a/crates/lspkit-treesitter/tests/parser.rs +++ b/crates/lspkit/tests/treesitter_parser.rs @@ -1,6 +1,8 @@ //! Coarse E2E tests for `LanguageParser` + `ParserError`. -use lspkit_treesitter::{LanguageParser, ParseCache, ParserError}; +#![cfg(feature = "treesitter")] + +use lspkit::treesitter::{LanguageParser, ParseCache, ParserError}; use tree_sitter::Language; struct RustParser; diff --git a/crates/lspkit-vfs/tests/vfs_change.rs b/crates/lspkit/tests/vfs_change.rs similarity index 99% rename from crates/lspkit-vfs/tests/vfs_change.rs rename to crates/lspkit/tests/vfs_change.rs index 91429bb..7c9f4f7 100644 --- a/crates/lspkit-vfs/tests/vfs_change.rs +++ b/crates/lspkit/tests/vfs_change.rs @@ -1,6 +1,6 @@ //! Integration tests for `Vfs::change` semantics. -use lspkit_vfs::{ +use lspkit::vfs::{ DocumentUri, DocumentVersion, Position, PositionEncoding, Range, TextEdit, Vfs, VfsError, }; diff --git a/crates/lspkit-vfs/tests/common/mod.rs b/crates/lspkit/tests/vfs_common/mod.rs similarity index 98% rename from crates/lspkit-vfs/tests/common/mod.rs rename to crates/lspkit/tests/vfs_common/mod.rs index 723f367..b1b828e 100644 --- a/crates/lspkit-vfs/tests/common/mod.rs +++ b/crates/lspkit/tests/vfs_common/mod.rs @@ -1,6 +1,6 @@ //! Shared fixtures for the vfs integration test binaries. -use lspkit_vfs::{ +use lspkit::vfs::{ DocumentUri, DocumentVersion, Position, PositionEncoding, Range, TextEdit, Vfs, VfsError, }; diff --git a/crates/lspkit-vfs/tests/edit_utf16.rs b/crates/lspkit/tests/vfs_edit_utf16.rs similarity index 97% rename from crates/lspkit-vfs/tests/edit_utf16.rs rename to crates/lspkit/tests/vfs_edit_utf16.rs index d24437a..054dd37 100644 --- a/crates/lspkit-vfs/tests/edit_utf16.rs +++ b/crates/lspkit/tests/vfs_edit_utf16.rs @@ -1,7 +1,8 @@ //! Integration tests for UTF-16 position handling, including surrogate pairs. -use lspkit_vfs::{DocumentUri, EditError, PositionEncoding, Vfs, VfsError}; +use lspkit::vfs::{DocumentUri, EditError, PositionEncoding, Vfs, VfsError}; +#[path = "vfs_common/mod.rs"] mod common; use common::{apply_one, edit}; diff --git a/crates/lspkit-vfs/tests/edit_utf32.rs b/crates/lspkit/tests/vfs_edit_utf32.rs similarity index 96% rename from crates/lspkit-vfs/tests/edit_utf32.rs rename to crates/lspkit/tests/vfs_edit_utf32.rs index 3b5d19f..c8b3e7d 100644 --- a/crates/lspkit-vfs/tests/edit_utf32.rs +++ b/crates/lspkit/tests/vfs_edit_utf32.rs @@ -1,7 +1,8 @@ //! Integration tests for UTF-32 (code-point) position handling. -use lspkit_vfs::{DocumentUri, PositionEncoding, Vfs, VfsError}; +use lspkit::vfs::{DocumentUri, PositionEncoding, Vfs, VfsError}; +#[path = "vfs_common/mod.rs"] mod common; use common::{apply_one, edit}; diff --git a/crates/lspkit-vfs/tests/edit_utf8.rs b/crates/lspkit/tests/vfs_edit_utf8.rs similarity index 98% rename from crates/lspkit-vfs/tests/edit_utf8.rs rename to crates/lspkit/tests/vfs_edit_utf8.rs index 7bff51c..11673eb 100644 --- a/crates/lspkit-vfs/tests/edit_utf8.rs +++ b/crates/lspkit/tests/vfs_edit_utf8.rs @@ -1,10 +1,11 @@ //! Integration tests for edits under `PositionEncoding::Utf8`. -use lspkit_vfs::{ +use lspkit::vfs::{ DocumentUri, DocumentVersion, EditError, Position, PositionEncoding, Range, TextEdit, Vfs, VfsError, }; +#[path = "vfs_common/mod.rs"] mod common; use common::{apply_one, edit}; diff --git a/crates/lspkit-vfs/tests/encoding.rs b/crates/lspkit/tests/vfs_encoding.rs similarity index 95% rename from crates/lspkit-vfs/tests/encoding.rs rename to crates/lspkit/tests/vfs_encoding.rs index 8b94a1b..2becd97 100644 --- a/crates/lspkit-vfs/tests/encoding.rs +++ b/crates/lspkit/tests/vfs_encoding.rs @@ -1,8 +1,8 @@ -//! Integration tests for `lspkit_vfs::encoding`. +//! Integration tests for `lspkit::vfs::encoding`. use std::str::FromStr; -use lspkit_vfs::{negotiate, PositionEncoding, UnknownEncoding}; +use lspkit::vfs::{negotiate, PositionEncoding, UnknownEncoding}; #[test] fn default_encoding_is_utf16() { diff --git a/crates/lspkit-vfs/tests/vfs_lifecycle.rs b/crates/lspkit/tests/vfs_lifecycle.rs similarity index 97% rename from crates/lspkit-vfs/tests/vfs_lifecycle.rs rename to crates/lspkit/tests/vfs_lifecycle.rs index ec02c61..3a80cea 100644 --- a/crates/lspkit-vfs/tests/vfs_lifecycle.rs +++ b/crates/lspkit/tests/vfs_lifecycle.rs @@ -1,6 +1,6 @@ //! Integration tests for `Vfs` open/close/read/version lifecycle. -use lspkit_vfs::{DocumentUri, DocumentVersion, PositionEncoding, Vfs}; +use lspkit::vfs::{DocumentUri, DocumentVersion, PositionEncoding, Vfs}; fn uri(path: &str) -> DocumentUri { DocumentUri::new(path) diff --git a/docs/plans/LSP-PROFILER-PLAN.md b/docs/plans/LSP-PROFILER-PLAN.md index bbbf468..d47a361 100644 --- a/docs/plans/LSP-PROFILER-PLAN.md +++ b/docs/plans/LSP-PROFILER-PLAN.md @@ -119,7 +119,7 @@ Production fixes from the filming audit: - [x] Keep allocation sizes and object counts visible at narrow panel widths; verify rendered Chromium output using a real heap capture [PROF-UI-MEMORY]. - [ ] Improve retention graph pixel density and label placement ([PROF-UI-REFGRAPH], issue #4). -- **Python runtime** (`crates/lsp-profiler-python`, `crates/lsp-profiler-helper`): not workspace members, no `Cargo.toml` for the python crate; 30 legacy `PROFILE-*` citations to rename; must adopt the stop-drain and the executeCommand mapping. +- **Python runtime** (`crates/lsp-profiler-python`, with the helper as its `[[bin]]`): excluded from the workspace, no `lib.rs` yet; 30 legacy `PROFILE-*` citations to rename; must adopt the stop-drain and the executeCommand mapping. - **Basilisk.** Its in-tree profiler, handlers, tests and VSIX were moved out of its working tree. **Basilisk has no profiler until it is rewired** onto this library and the Python runtime. - **Dart, Rust/native runtimes.** Not specified [PROF-SCOPE]. Each is one crate implementing `Runtime`; nothing above the trait changes. diff --git a/docs/plans/crate-consolidation-plan.md b/docs/plans/crate-consolidation-plan.md new file mode 100644 index 0000000..0f29112 --- /dev/null +++ b/docs/plans/crate-consolidation-plan.md @@ -0,0 +1,57 @@ +# Plan — consolidate lspkit crates by hierarchy + +We are collapsing 12 crates into 5, grouped by dependency layer, and sizing each one near the median popular crate (about 3k lines). The evidence is in [docs/reports/crate-size-survey.md](../reports/crate-size-survey.md). This is a prerequisite for the first tagged release in [publishing-plan.md](publishing-plan.md), because every crate name we publish is permanent. + +## Target layout `[CRATE-LAYOUT]` + +```mermaid +graph TD + S["lspkit-server (L1)"] --> L["lspkit (L0)"] + D["lsp-profiler-dotnet (L1)"] --> P["lsp-profiler (L0)"] + Y["lsp-profiler-python (L1, parked)"] --> P +``` + +| Crate | Layer | Absorbs | ~Lines | Published | +|---|---|---|---|---| +| `lspkit` | L0 | `lspkit`, `lspkit-config`, `lspkit-vfs`, `lspkit-treesitter`, `lspkit-sidecar` | 3.8k | yes | +| `lspkit-server` | L1 | `lspkit-server`, `lspkit-live`, `lspkit-mcp` | 8.5k | yes | +| `lsp-profiler` | L0 | — | 6.2k | yes | +| `lsp-profiler-dotnet` | L1 | — | 3.9k | yes | +| `lsp-profiler-python` | L1 | `lsp-profiler-python`, `lsp-profiler-helper` | 12.9k | no (parked) | + +- **`[CRATE-LAYOUT-LAYERS]`** An L1 crate depends only on its own tree's L0 crate. An L0 crate has no internal dependencies. +- **`[CRATE-LAYOUT-MODULES]`** Each absorbed crate becomes a top-level module with the same name: `lspkit::config`, `lspkit::vfs`, `lspkit::treesitter`, `lspkit::sidecar`, `lspkit_server::live`, `lspkit_server::mcp`. +- **`[CRATE-LAYOUT-FEATURES]`** `lspkit-server`'s `mcp` cargo feature (default off) keeps the MCP dependency out of LSP-only consumers. `treesitter` and `sidecar` become features of `lspkit` only if their dependencies are heavy. +- **`[CRATE-LAYOUT-HELPER]`** `lsp-profiler-helper` becomes a `[[bin]]` target of `lsp-profiler-python`. + +## Retiring published names `[CRATE-RETIRE]` + +The six absorbed `lspkit-*` names are already on crates.io at 0.0.1. Audit date: 2026-10-31. + +| Crate | Downloads | Reverse deps | Action | +|---|---|---|---| +| `lspkit-config` | 19 | 0 | Delete on crates.io | +| `lspkit-treesitter` | 21 | 0 | Delete on crates.io | +| `lspkit-sidecar` | 20 | 0 | Delete on crates.io | +| `lspkit-mcp` | 20 | 0 | Delete on crates.io | +| `lspkit-live` | 932 | 0 | Delete on crates.io | +| `lspkit-vfs` | 982 | 1 (`lspkit-server` 0.0.1) | `cargo yank --version 0.0.1 lspkit-vfs`, plus a deprecation README pointing to `lspkit::vfs` | + +Only a crate owner can delete a crate. Use the Settings → Delete crate page on crates.io, and do it after the consolidated release ships so consumers always have somewhere to go. + +## TODO + +- [x] Add the `[CRATE-LAYOUT]` and `[CRATE-RETIRE]` sections to [lspkit-spec.md](../specs/lspkit-spec.md), with this plan as the implementation tracker. +- [x] Move `lspkit-config`, `lspkit-vfs`, `lspkit-treesitter` and `lspkit-sidecar` into `crates/lspkit/src/` as modules. Merge their dependencies and tests, and delete the old crate directories. +- [x] Move `lspkit-live` and `lspkit-mcp` into `crates/lspkit-server/src/` as modules, with `mcp` behind a feature. +- [x] Fold `lsp-profiler-helper` into `lsp-profiler-python` as a `[[bin]]`. +- [x] Update the workspace `members`, `exclude` and `[workspace.dependencies]` in `Cargo.toml`, then relock. +- [x] Update every `use lspkit_*` path in this repo, its tests, docs, skills and `coverage-thresholds.json`. Each absorbed crate keeps its floor as a `::` key (e.g. `lspkit::vfs`), which `_coverage_check` now gates. +- [ ] Update the consumers (SharpLsp, Deslop, Basilisk, lql-lsp-rust) to the new paths. +- [x] Cut the `[REL-ORDER]` publish list in `release.yml`, [publishing-plan.md](publishing-plan.md) and the release setup doc to `lsp-profiler`, `lsp-profiler-dotnet`, `lspkit`, `lspkit-server`. +- [x] `cargo package --workspace --locked` verifies all 4 published crates (2026-10-31). +- [x] `diagnostic_reference unrepresentable` runs on Linux; on Apple, a sibling test asserts that APFS refuses the non-UTF-8 name (`EILSEQ`). +- [x] Fix the 8 oxlint residuals in `packages/` (2026-10-31). The sync `fs` reads are now async and awaited at every caller, and `PROFILER_REVIEW_HTML` is replaced by `registerCpuCaptureContract(capture, { reviewHtmlPath })`. `npm run lint` is clean; 190 + 107 tests pass. +- [x] Confirm `make ci` is green on the PR's Linux CI (PR #37, ubuntu-22.04, 2026-10-31: every check passes and every coverage floor holds, with `lspkit-server::live` at 94.82% ≥ 94%). On macOS, `lspkit-server::live` stays at 92.23% < 94%: the uncovered watcher lines are the inotify `Access` and rescan paths plus race-only drop paths. +- [ ] Ship the first tagged release (see [publishing-plan.md](publishing-plan.md)). +- [ ] Retire the old names per `[CRATE-RETIRE]`: delete 5 crates on crates.io, and yank and deprecate `lspkit-vfs`. diff --git a/docs/plans/publishing-plan.md b/docs/plans/publishing-plan.md index e4e874a..163804d 100644 --- a/docs/plans/publishing-plan.md +++ b/docs/plans/publishing-plan.md @@ -1,31 +1,8 @@ -# Plan — Publish `lspkit-*` to crates.io tonight +# Plan — release-only publishing for crates.io and npm -Implements [REL-TRIGGER], [REL-OIDC], [REL-VERSION], [REL-ORDER], [REL-GATES], [REL-METADATA] from [docs/specs/lspkit-spec.md](../specs/lspkit-spec.md). Approach modelled on Shipwright's release pipeline; OIDC mechanism per `rust-lang/crates-io-auth-action` (GA 2025). +Implements `[REL-REGIME]`, `[REL-TRIGGER]`, `[REL-VERSION]`, `[REL-GATES]`, `[REL-GITHUB]`, `[REL-OIDC]`, `[REL-ORDER]`, `[REL-METADATA]`, `[REL-PACKAGED-BUILD]` and `[REL-DEPENDABOT]` from [docs/specs/lspkit-spec.md](../specs/lspkit-spec.md#release--publishing). The model is Shipwright's deployment contract plus the AgentPMO `[CI-WORKFLOWS]` standard: CI gates pull requests, and only a version tag ships. -## Dep graph → publish order - -Topo sort of internal deps. Publish leaves first, dependents second. **No sleep** between batches — `cargo publish` >= 1.66 polls the sparse index itself until the just-published crate is resolvable (see Shipwright [release.yml:156–158](../../../Shipwright/.github/workflows/release.yml#L156-L158)), so the next `cargo publish` resolves its `lspkit-*` workspace deps without a manual wait. - -```mermaid -flowchart LR - subgraph B1["Batch 1 — leaves"] - lspkit - lspkit-vfs - lspkit-config - lspkit-sidecar - lspkit-treesitter - end - subgraph B2["Batch 2 — dependents"] - lspkit-server - lspkit-mcp - lspkit-live - end - B1 -- "cargo publish polls index" --> B2 - lspkit --> lspkit-server - lspkit-vfs --> lspkit-server - lspkit --> lspkit-mcp - lspkit --> lspkit-live -``` +One-time setup and the release command live in [docs/release-setup.md](../release-setup.md). ## Release flow @@ -33,251 +10,33 @@ flowchart LR sequenceDiagram autonumber actor Dev as Maintainer - participant Repo as GitHub repo participant GHA as release.yml - participant Crates as crates.io participant Rel as GitHub Release - - Dev->>Repo: git tag -s v0.0.1 && git push --tags - Repo->>GHA: workflow_run (tag push) - GHA->>GHA: stamp 0.0.0-dev → 0.0.1 (working tree dirty, not committed) - GHA->>GHA: make ci (lint + test + cov + build) - GHA->>GHA: cargo deny check --locked - GHA->>Crates: rust-lang/crates-io-auth-action (OIDC exchange) - Crates-->>GHA: short-lived registry token - loop Batch 1 then Batch 2 (cargo polls index between) - GHA->>Crates: cargo publish --locked --allow-dirty -p - end - GHA->>Rel: create release + attest provenance -``` - -## Source-of-truth versioning - -- `[workspace.package].version = "0.0.0-dev"` in [Cargo.toml](../../Cargo.toml). -- Every crate's `Cargo.toml` uses `version.workspace = true` (already true). -- `[workspace.dependencies]` pins for `lspkit-*` use `version = "0.0.0-dev"` matching the placeholder; the stamp step rewrites both the package version *and* the internal `[workspace.dependencies]` pins with `toml_edit` so structural rewrite is exact (no sed, no regex on TOML). -- Stamp is workflow-only; never committed back to `main`. - -## OIDC trusted publisher registration - -Per [REL-OIDC], each of the eight crates needs a Trusted Publisher row on crates.io before the first release. Settings → `Trusted publishers` → `Add GitHub publisher`: - -| Field | Value | -|---|---| -| Owner | `Nimblesite` | -| Repository | `lspkit` | -| Workflow filename | `release.yml` | -| Environment | `release` | - -Until a crate exists on crates.io, the publisher is registered as a **pending** trusted publisher at `https://crates.io/settings/pending-trusted-publishers`. First publish reserves the name and graduates the publisher. - -## Workflow skeleton (`.github/workflows/release.yml`) - -Mirrors Shipwright's [release.yml](../../../Shipwright/.github/workflows/release.yml) — identical permission shape, identical action pin, identical idempotent-publish wrapper. Per-crate steps are explicit (not a loop) so failures bisect by step name. - -```yaml -name: Release -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+-*' - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false - -# Top-level: id-token: write so any job in this workflow can mint an OIDC -# token. Job-scoping is fine too; Shipwright uses top-level because it -# also publishes npm/NuGet OIDC. We only publish crates, but keep parity. -permissions: - contents: read - id-token: write - -jobs: - publish: - name: Publish to crates.io - runs-on: ubuntu-latest - timeout-minutes: 30 - environment: release # MUST match the crates.io trusted-publisher row - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable - with: { toolchain: stable, components: rustfmt,clippy,llvm-tools-preview } - - uses: Swatinem/rust-cache@ - with: { shared-key: release } - - uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2 - with: { tool: cargo-llvm-cov,cargo-nextest,cargo-deny } - - # DIAGNOSTIC (keep for first releases; remove once stable). Prints the - # exact OIDC claims crates.io will match against your Trusted Publisher - # row. Compare repository / workflow_ref / environment / ref char-by-char - # if the publish step ever returns "invalid trusted publisher". - - name: Decode OIDC claims (diagnostic) - run: | - RESP=$(curl -sf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ - "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=crates.io") || { echo "claim fetch failed"; exit 0; } - echo "$RESP" | python3 -c "import sys,json,base64; t=json.load(sys.stdin)['value']; p=t.split('.')[1]; p+='='*(-len(p)%4); c=json.loads(base64.urlsafe_b64decode(p)); print(json.dumps({k:c.get(k) for k in ['repository','repository_owner','workflow_ref','job_workflow_ref','environment','ref','ref_type']}, indent=0))" - - # Stamp dirties the working tree on purpose. `--allow-dirty` is required - # on every cargo publish call downstream because we never commit the bump. - - name: Stamp workspace version from tag - run: scripts/stamp-version.sh "${GITHUB_REF_NAME#v}" - - - name: CI gate - run: make ci - - - name: Supply-chain gate - run: cargo deny check --locked - - # OIDC → short-lived registry token. The action sets `audience=crates.io` - # for you; the GitHub OIDC token signs the claim, crates.io verifies the - # Trusted Publisher row, returns a token in steps.crates_auth.outputs.token. - # No CARGO_REGISTRY_TOKEN secret stored anywhere. - - name: Authenticate to crates.io via OIDC - id: crates_auth - uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 - - # Batch 1 — leaves (no internal lspkit-* deps). cargo publish (>=1.66) - # polls the index after upload, so the next step resolves cleanly. - - name: Publish lspkit - run: scripts/cargo-publish-or-skip.sh lspkit - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - - name: Publish lspkit-vfs - run: scripts/cargo-publish-or-skip.sh lspkit-vfs - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - - name: Publish lspkit-config - run: scripts/cargo-publish-or-skip.sh lspkit-config - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - - name: Publish lspkit-sidecar - run: scripts/cargo-publish-or-skip.sh lspkit-sidecar - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - - name: Publish lspkit-treesitter - run: scripts/cargo-publish-or-skip.sh lspkit-treesitter - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - # Batch 2 — depend on Batch 1. - - name: Publish lspkit-server - run: scripts/cargo-publish-or-skip.sh lspkit-server - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - - name: Publish lspkit-mcp - run: scripts/cargo-publish-or-skip.sh lspkit-mcp - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } - - - name: Publish lspkit-live - run: scripts/cargo-publish-or-skip.sh lspkit-live - env: { CARGO_REGISTRY_TOKEN: "${{ steps.crates_auth.outputs.token }}" } -``` - -## Helper scripts - -Both Bash, both `set -uo pipefail`, both idempotent. Lifted directly from Shipwright's [scripts/cargo-publish-or-skip.sh](../../../Shipwright/scripts/cargo-publish-or-skip.sh). - -`scripts/stamp-version.sh ` — uses `toml-cli` (or `toml_edit` via a tiny Rust shim) to rewrite `[workspace.package].version` and every `lspkit-*` row under `[workspace.dependencies]` with the exact ``. Structural rewrite only, never regex on TOML. Verifies all eight crate manifests resolve identically. - -`scripts/cargo-publish-or-skip.sh ` — one crate per invocation; treats `already uploaded` / `already exists` as success so reruns are safe: - -```bash -#!/usr/bin/env bash -set -uo pipefail -crate="${1:?usage: $0 }" -output=$(cargo publish --locked --allow-dirty -p "$crate" 2>&1) -code=$? -printf '%s\n' "$output" -if [ "$code" -ne 0 ] && printf '%s' "$output" | grep -qE 'is already uploaded|already exists'; then - printf '::notice::%s at this version is already on crates.io — skipping\n' "$crate" - exit 0 -fi -exit "$code" -``` - -`--allow-dirty` is mandatory: the stamp step mutates `Cargo.toml`/`Cargo.lock` without committing (source keeps the `0.0.0-dev` placeholder), so the working tree is intentionally dirty at publish time. - -## Metadata gap to close before first tag - -Every crate's `Cargo.toml` currently lacks `description`, `readme`, `documentation`, and `keywords`. `cargo publish` will fail without `description`. Each needs: - -```toml -description = "" -readme = "README.md" # workspace-root README is fine; cargo packages it -documentation = "https://docs.rs/" -keywords = ["lsp", "mcp", "language-server"] # ≤5 -categories = ["development-tools"] # validate against crates.io list + participant Crates as crates.io + participant Npm as npm + + Dev->>GHA: push tag vX.Y.Z + GHA->>GHA: CodeQL gate on the tagged SHA (blocks on High/Critical) + GHA->>GHA: npm ci, stamp 0.0.0-dev → X.Y.Z, cargo update --workspace + GHA->>GHA: make ci, cargo deny, cargo package --workspace, npm pack + GHA->>GHA: SBOM, SHA256SUMS + cosign, provenance + SBOM attestations + GHA->>Rel: create vX.Y.Z with every tarball, SBOM and signed sums + GHA->>Crates: OIDC token, then 4 crates in dependency order + GHA->>Npm: OIDC + --provenance, 2 packages ``` -The single `LICENSE-MIT` file at the workspace root is staged into each crate dir at publish time (the `cp` step in `release.yml`) so the published tarball ships the license text — `license-file` is **not** needed since the SPDX `license = "MIT"` is set. `[SWR-REL-LICENSE]` - ## Rollback -Crates.io does not allow unpublishing past a 72-hour window; even within it, only yanking is supported. Therefore rollback is **yank + new patch**: - -```mermaid -flowchart LR - bad[bad v0.0.1 published] --> yank[cargo yank --version 0.0.1 -p crate] - yank --> fix[fix on main] - fix --> tag[git tag v0.0.2] - tag --> rep[re-run release.yml] -``` - -Yanking is reversible (`cargo yank --undo`); deletion is not. Plan for forward fixes only. - ---- - -## TODO — going live tonight - -Order matters; each line is one PR or one step. Strike through when done. - -### Pre-release setup (must land before tag) - -- [ ] Change `[workspace.package].version` in [Cargo.toml](../../Cargo.toml) from `0.0.1` → `0.0.0-dev`. -- [ ] Change every `lspkit-*` row under `[workspace.dependencies]` in the same file from `version = "0.0.1"` → `version = "0.0.0-dev"`. -- [ ] Add `description`, `readme = "../../README.md"`, `documentation`, `keywords`, `categories`, `include` to each of the eight `crates/lspkit*/Cargo.toml` files. -- [x] Add workspace-root `LICENSE-MIT` (the `Cargo.toml` declares `license = "MIT"`; staged into each crate at publish time). -- [ ] Add `deny.toml` at repo root with advisory/license/source/bans tables; minimum: deny `unsafe-code`, deny unmaintained, allow `MIT/Apache-2.0/BSD-*/Unicode-DFS-2016/CC0-1.0`. -- [ ] Add `scripts/stamp-version.sh` (uses `toml-cli`; rewrites `[workspace.package].version` and `lspkit-*` rows under `[workspace.dependencies]`; verifies all eight resolve identically). -- [ ] Add `scripts/cargo-publish-or-skip.sh` (copy from Shipwright; one crate per call; idempotent skip on `already uploaded`/`already exists`; `--allow-dirty --locked`). -- [ ] Add `.github/workflows/release.yml` per the skeleton above; pin every `uses:` to a 40-char commit SHA; declare `environment: release`. -- [ ] Create a `release` Environment in repo settings → Environments; add maintainer as required reviewer; restrict to tags matching `v*.*.*`. -- [ ] Configure Dependabot for GitHub Actions in `.github/dependabot.yml` (so SHA pins get PR'd forward). - -### crates.io trusted publisher registration (one-time, all eight crates) - -> **Web-UI only.** crates.io's trusted-publishing API rejects `cargo login` tokens (verified: HTTP 403, -> insufficient scope), so this is done at crates.io → *Account/crate → Trusted Publishing → Add* using a -> **pending publisher** for each not-yet-published name. The account that registers the pending publisher -> becomes the crate's **initial owner** — ownership is moved to the Nimblesite org team below after first publish. - -- [ ] `lspkit` — register pending trusted publisher (owner `Nimblesite`, repo `lspkit`, workflow `release.yml`, env `release`). -- [ ] `lspkit-vfs` — same. -- [ ] `lspkit-config` — same. -- [ ] `lspkit-sidecar` — same. -- [ ] `lspkit-treesitter` — same. -- [ ] `lspkit-server` — same. -- [ ] `lspkit-mcp` — same. -- [ ] `lspkit-live` — same. - -### Rehearsal (local, no token needed) - -- [ ] Run `scripts/stamp-version.sh 0.0.1` locally; confirm `grep -r 0.0.0-dev crates/ Cargo.toml` returns zero. -- [ ] Run `for c in lspkit lspkit-vfs lspkit-config lspkit-sidecar lspkit-treesitter lspkit-server lspkit-mcp lspkit-live; do cargo publish --dry-run --locked --allow-dirty -p $c; done` locally; all eight crates pass. -- [ ] `git restore Cargo.toml crates/*/Cargo.toml` to undo the stamp (the workflow re-stamps on its own checkout). - -### Go-live - -- [ ] Merge all the above into `main`; CI green. -- [ ] `git tag -s v0.0.1 -m "lspkit 0.0.1"` and `git push origin v0.0.1`. -- [ ] Watch `release.yml`; approve the `release` environment gate. -- [ ] Verify all eight crates appear at `https://crates.io/crates/lspkit*` with version `0.0.1`. -- [ ] Verify `https://docs.rs/lspkit` (and the seven siblings) builds within ~30 min. -- [ ] Verify the GitHub Release page lists the tag with auto-generated notes. +Neither registry allows deleting a version. On crates.io, `cargo yank --version X.Y.Z -p `. On npm, `npm deprecate @X.Y.Z ""`. Then fix forward on `main` and tag a new patch. -### Post-release (next day) +## TODO -- [ ] Bump consumer repos ([lql-lsp-rust](https://github.com/Nimblesite/lql-lsp-rust), Basilisk, Deslop, SharpLsp) to depend on `lspkit-* = "0.0.1"` from crates.io instead of `path = "..."`. -- [ ] Open follow-up issue: `lspkit-vfs::edit::position_to_byte` UTF-8 branch missing bounds-check on `pos.character` — return `EditError::OutOfBounds` instead of letting `ropey` panic. (Latent from the bootstrap PR.) -- [ ] Open follow-up: provenance/SBOM attestation for crates.io packages once `actions/attest-sbom` lands stable support for Rust. +- [x] Release-only triggers: `ci.yml`, `supply-chain.yml` and `codeql.yml` run on PRs (plus a weekly cron for the scanners), and `release.yml` runs on `v*` tags. +- [x] Source versions are `0.0.0-dev`, stamped from the tag. +- [x] `release.yml` runs the CodeQL gate, `make ci`, cargo-deny, `npm audit` and `cargo package --workspace`. It then produces the SBOM, cosign-signed `SHA256SUMS`, provenance attestations and the GitHub Release, and publishes 4 crates and 2 npm packages via OIDC. +- [x] `lsp-profiler` builds from its published tarball ([PROF-WIRE-PACKAGED]). +- [x] Dependabot staging branch config and auto-merge workflow. +- [x] zizmor and osv-scanner (Cargo.lock + both npm lockfiles) in `supply-chain.yml`. +- [x] The `release` environment only deploys from `v*.*.*` tags. +- [ ] Manual steps 1–4 in [docs/release-setup.md](../release-setup.md). +- [ ] First tagged release (`v0.0.2`). diff --git a/docs/plans/website-plan.md b/docs/plans/website-plan.md new file mode 100644 index 0000000..3764e2b --- /dev/null +++ b/docs/plans/website-plan.md @@ -0,0 +1,15 @@ +# Website sketch + +- [x] Read the repository spec and techdoc package contract. +- [x] Define the design system [WEB-DESIGN]. +- [x] Build the Eleventy shell and one illustrated example post [WEB-CONTENT]. +- [x] Verify the build and generated links [WEB-ACCESS]. +- [ ] Visually verify responsive layout and interactions: blocked because the computer-use browser host is unavailable. +- [x] Check duplication: baseline and final CLI scans found no clusters involving `website/`; existing repository findings remain outside this sketch. +- [x] Clean temporary output and check disk space: stopped the owned preview server, removed generated `_site/`; about 13 GiB remains available. Dependencies remain for local development. + +Final checks: Oxlint passed with warnings treated as errors; the final Eleventy build passed. + +Scope: a local starter in `website/`, using the existing lspkit identity. Publishing is deferred to the repository's release workflow; repository instructions prohibit git operations without an explicit request. No changes to crates or existing CI gates. + +Validation: Eleventy generated twelve files, including eight HTML pages. Parsed every HTML page to check local links, asset references, fragments, image alternatives, a single H1 and JSON-LD validity. HTTP checks passed for homepage, docs, blog index, article and illustration. A read-only Kimi k3.1-pro review informed theme and mobile CSS; its recommendations were reviewed against the installed plugin. The plugin's menu lacks initial `aria-expanded` and `aria-controls` metadata, although toggling sets the expanded state; this upstream limitation is retained rather than copying its templates into the sketch. diff --git a/docs/release-setup.md b/docs/release-setup.md new file mode 100644 index 0000000..efa4275 --- /dev/null +++ b/docs/release-setup.md @@ -0,0 +1,11 @@ +# Release setup + +Remaining one-time steps. Trusted Publishers (`Nimblesite/lspkit`, `release.yml`, environment `release`) are already attached to the crates `lsp-profiler`, `lsp-profiler-dotnet`, `lspkit` and `lspkit-server`, and to the npm package `@nimblesite/lsp-profiler-ui`, which also requires 2FA and disallows tokens. + +1. **Delete the absorbed crates** [CRATE-RETIRE], in this order: `lspkit-live`, `lspkit-vfs`, `lspkit-config`, `lspkit-mcp`, `lspkit-sidecar`, `lspkit-treesitter`. Open `https://crates.io/crates//settings` → Delete crate, then revoke the bootstrap token. +2. **GitHub settings:** + - Environments → `release`: add yourself as Required reviewer. + - Rulesets → `main protection`: set it to Active, requiring a PR and the `CI` check. + - Code security: turn on Dependabot security updates. +3. **After this branch merges:** `git push origin main:dependabot-upgrades` (leave it unprotected). +4. **Release:** `gh release create v0.0.2 --target main --generate-notes`, then approve the `release` gate. Re-run a failed run to retry. diff --git a/docs/reports/crate-size-survey.md b/docs/reports/crate-size-survey.md new file mode 100644 index 0000000..fcb5fe1 --- /dev/null +++ b/docs/reports/crate-size-survey.md @@ -0,0 +1,83 @@ +# Crate size survey and proposed lspkit crate hierarchy + +How big is a typical Rust crate, and how should lspkit's 12 crates collapse to match? Measured 2026-10-31. + +## Method + +Nobody publishes a lines-of-code distribution for crates.io, so we measured one. We downloaded each crate's published `.crate` tarball from `static.crates.io`, counted non-blank lines in every `.rs` file, then deleted the tarball. + +- **Popular:** the 199 most-downloaded crates (crates.io API, `sort=downloads`). +- **Random:** 83 crates, each picked at random from the results of a crates.io search for a random dictionary word. The top-199 crates were excluded. + +## Results (non-blank Rust lines per crate) + +| Sample | n | p10 | p25 | **median** | p75 | p90 | mean | +|---|---|---|---|---|---|---|---| +| Popular, all `.rs` | 199 | 231 | 759 | **2,958** | 8,278 | 23,820 | 13,116 | +| Popular, `src/` only | 198 | 206 | 656 | **2,328** | 7,747 | 21,069 | 11,695 | +| Random, all `.rs` | 83 | 156 | 558 | **1,436** | 4,872 | 18,474 | 9,196 | +| Random, `src/` only | 81 | 141 | 436 | **1,278** | 4,488 | 21,922 | 8,628 | + +Reference points from the popular sample: `cfg-if` 215 · `autocfg` 1.2k · `anstream` 2.5k · `tracing-core` 5.9k · `tower` 12.7k · `ring` 24.9k. + +The mean is useless as a target, because a few giants (`syn`, `windows-*`, `ring`) drag it up to 4–9× the median. **Target the popular median: about 3k lines, with anything from about 0.8k to 8k (the popular p25–p75) acceptable.** Crates below about 750 lines are smaller than three-quarters of popular crates. + +## lspkit today + +| Crate | Lines | Internal deps | vs. popular median | +|---|---|---|---| +| `lspkit-config` | 250 | — | far under | +| `lspkit-treesitter` | 310 | — | far under | +| `lsp-profiler-helper` | 476 | (parked) | far under | +| `lspkit-mcp` | 531 | `lspkit` | far under | +| `lspkit` | 776 | — | under | +| `lspkit-vfs` | 1,011 | — | under | +| `lspkit-live` | 1,037 | `lspkit` | under | +| `lspkit-sidecar` | 1,460 | — | under | +| `lsp-profiler-dotnet` | 3,870 | `lsp-profiler` | on target | +| `lsp-profiler` | 6,180 | — | in range | +| `lspkit-server` | 6,939 | `lspkit`, `lspkit-vfs` | in range | +| `lsp-profiler-python` | 12,471 | (parked) | over | + +Eight of the 12 crates are below the popular median, and four are below its p25. + +## Proposed structure: by hierarchy + +Group crates by **dependency layer**, not by feature. Two trees (toolkit and profiler) with two layers each. Every crate in a layer depends only on the layer below it. + +```mermaid +graph TD + subgraph Toolkit + L["lspkit — L0 foundation
EngineApi · config · vfs · treesitter · sidecar
~3.8k"] + S["lspkit-server — L1 runtime
LSP server · live · mcp (feature)
~8.5k"] + S --> L + end + subgraph Profiler + P["lsp-profiler — L0 engine
~6.2k"] + D["lsp-profiler-dotnet — L1 runtime
~3.9k"] + Y["lsp-profiler-python — L1 runtime (parked)
+ helper as a [[bin]]
~12.9k"] + D --> P + Y --> P + end +``` + +| Proposed crate | Layer | Absorbs | Lines | Rationale | +|---|---|---|---|---| +| `lspkit` | L0 | `lspkit`, `-config`, `-vfs`, `-treesitter`, `-sidecar` | ~3.8k | Every crate with no internal deps. At the median. | +| `lspkit-server` | L1 | `-server`, `-live`, `-mcp` | ~8.5k | Everything that builds on `lspkit`. An `mcp` cargo feature keeps the MCP dependency out of LSP-only consumers. Popular p75. | +| `lsp-profiler` | L0 | — | ~6.2k | Language-neutral engine. Unchanged. | +| `lsp-profiler-dotnet` | L1 | — | ~3.9k | Runtime plugin. Separate so non-.NET servers don't pull it in. | +| `lsp-profiler-python` | L1 | `-python`, `-helper` | ~12.9k | Parked and not published. The elevated helper becomes a `[[bin]]` of its runtime. | + +That gives **12 → 5 crates, 4 published**. The published crates average about 5.6k lines and every one sits between the popular p25 and p75. + +Consequences: + +- The 8 `lspkit-*` names already on crates.io at 0.0.1 shrink to 2. +- Each of the 6 retired names needs a final release that re-exports from the new crate, plus a deprecation notice, then no further releases. +- The `[REL-ORDER]` publish list in `release.yml` drops to `lsp-profiler`, `lsp-profiler-dotnet`, `lspkit`, `lspkit-server`. + +## Sources + +- crates.io API (`/api/v1/crates`) and `static.crates.io` tarballs, sampled 2026-10-31. +- No published crates.io lines-of-code study was found. Tools such as [cargo-loc](https://rust-digger.code-maven.com/crates/cargo-loc) and [rustloc](https://docs.rs/crate/rustloc/0.5.0) count lines but publish no registry-wide statistics. diff --git a/docs/specs/LSP-PROFILER-SPEC.md b/docs/specs/LSP-PROFILER-SPEC.md index 9cb4391..06c4fb8 100644 --- a/docs/specs/LSP-PROFILER-SPEC.md +++ b/docs/specs/LSP-PROFILER-SPEC.md @@ -69,7 +69,7 @@ flowchart LR | `lsp-profiler-dotnet` | `crates/lsp-profiler-dotnet` | .NET runtime: process classification, engine driver, mapping engine output to the neutral model | | `lsp-profiler-dotnet-engine` | `dotnet/LspProfiler.DotNet.Engine` | C# tool that does the .NET capture and analysis ([PROF-DOTNET-ENGINE]) | | `lsp-profiler-python` | `crates/lsp-profiler-python` | Python runtime (parked, [PROF-PY]) | -| `lsp-profiler-helper` | `crates/lsp-profiler-helper` | Elevated macOS sampling helper (parked with Python, [PROF-HELPER-PROTOCOL]) | +| `lsp-profiler-helper` | `crates/lsp-profiler-python/src/bin/lsp-profiler-helper` | Elevated macOS sampling helper (parked with Python, [PROF-HELPER-PROTOCOL]) | | `lsp-profiler-ui` | `packages/lsp-profiler-ui` | TypeScript UI library for VS Code-family editors ([PROF-UI]) | ## [PROF-LSP] LSP mapping @@ -136,6 +136,10 @@ A memory diagnostic about type `T` offers "Show what keeps `T` alive" and "List Comments directly above a type, variant or field become doc comments on both sides ([PROF-WIRE-GEN-DOCS]). Generated files are never edited by hand. A build fails if they are stale. +#### [PROF-WIRE-PACKAGED] Published crate build + +A crates.io tarball has no workspace, so the generator is absent. `lsp-profiler` packs the generated `src/wire_generated.rs` and its LICENSE through the crate's `include`. `build.rs` runs the generator when `scripts/typediagram/generate.mjs` exists and otherwise requires the packed model, failing with a precise error if it is missing. + #### [PROF-WIRE-GEN-RUST] Rust emission Serde structs and externally tagged enums. @@ -804,7 +808,7 @@ Same-user processes attach through the diagnostics IPC without elevation. Anothe ## [PROF-PY] Python runtime (parked) -Runtime ID `python`. The code moved from Basilisk into `crates/lsp-profiler-python` (with `crates/lsp-profiler-helper`). It is not a workspace member until it is un-parked. Its citations of the legacy Basilisk IDs map onto the IDs below when it rejoins. +Runtime ID `python`. The code moved from Basilisk into `crates/lsp-profiler-python`, which also hosts the `lsp-profiler-helper` binary (see [Crate layout](lspkit-spec.md#crate-layout)). It is not a workspace member until it is un-parked. Its citations of the legacy Basilisk IDs map onto the IDs below when it rejoins. ### [PROF-PY-SCAFFOLD] Frame filter diff --git a/docs/specs/lspkit-spec.md b/docs/specs/lspkit-spec.md index e7a7334..78e331f 100644 --- a/docs/specs/lspkit-spec.md +++ b/docs/specs/lspkit-spec.md @@ -2,56 +2,48 @@ ## Identity -`lspkit` is a public, fully generic Rust workspace published as `lspkit-*` crates that supplies the cross-cutting infrastructure every LSP+MCP server reimplements. Its headline contribution is the **"one engine, two surfaces"** contract: a single `EngineApi` trait that an LSP backend and an MCP server both consume, so live analysis state computed once is vended two ways. +`lspkit` is a public, fully generic Rust workspace published as four crates — `lspkit`, `lspkit-server`, `lsp-profiler` and `lsp-profiler-dotnet` — that supplies the cross-cutting infrastructure every LSP+MCP server reimplements. Its headline contribution is the **"one engine, two surfaces"** contract: a single `EngineApi` trait that an LSP backend and an MCP server both consume, so live analysis state computed once is vended two ways. ## Hard invariants These are non-negotiable. Any patch violating one is illegal. - **Generic only.** Toolkit code, docs, READMEs, comments, and examples MUST NOT reference any specific consumer, domain, language ecosystem, or product. A grep for known consumer names returns zero hits in `crates/` and `README.md`. -- **No language-specific backend code.** No Roslyn, FCS, Pyright, Ruff, tree-sitter grammar bundling, nor any other language ecosystem glue lives in any crate. `lspkit-sidecar` is pure transport — its public surface MUST NOT name any backend technology. +- **No language-specific backend code.** No Roslyn, FCS, Pyright, Ruff, tree-sitter grammar bundling, nor any other language ecosystem glue lives in any crate. `lspkit::sidecar` is pure transport — its public surface MUST NOT name any backend technology. - **Maintained bedrock or self-owned.** Toolkit MUST NOT depend on unmaintained crates for core protocol handling. `tower-lsp` is forbidden (last release 2023). The toolkit depends on `lsp-types` (maintained) and ships its own JSON-RPC server loop inside `lspkit-server`. -- **Third-party LSP/MCP types stay hidden.** No `rmcp::*` or `lsp_types::*` request/response type appears in a `pub` signature of any `lspkit-*` crate. Newtype wrappers are required at every boundary. +- **Third-party LSP/MCP types stay hidden.** No `rmcp::*` or `lsp_types::*` request/response type appears in a `pub` signature of `lspkit` or `lspkit-server`. Newtype wrappers are required at every boundary. - **No wire-format coupling.** The toolkit ships no typeDiagram-generated types. Consumers define their own wire schemas case-by-case. -- **Locks never span `.await`.** `lspkit-vfs` MUST scope all `DashMap` and `Mutex` guards inside synchronous blocks; methods MUST NOT return guards. +- **Locks never span `.await`.** `lspkit::vfs` MUST scope all `DashMap` and `Mutex` guards inside synchronous blocks; methods MUST NOT return guards. ## Public crate inventory -Eight crates. The split is locked. +Four published crates. The split is locked. | Crate | Public surface (intent) | |---|---| -| **lspkit** | Façade. Exposes `EngineApi` trait + `Generation`, `Snapshot`, `GenerationEvent`, `Cause`, `RescanScope`, `RescanTicket` types. Feature `tracing-setup` exposes a stdio-safe `tracing-subscriber` builder. `prelude` module re-exports the headline items. | -| **lspkit-server** | LSP server scaffolding built on `lsp-types`: Content-Length framing, JSON-RPC request/response/notification dispatcher, capability builder, URI ↔ `PathBuf` helpers, workspace-folders abstraction, diagnostics fan-out bus, progress sink, cancellation plumbing. Consumers register handlers; the crate routes them to an `EngineApi` + VFS pair. | -| **lspkit-vfs** | Concurrent open-document store with LSP version tracking, `didOpen` / `didChange` / `didClose` helpers, rope-based incremental edits, and position-encoding negotiation (UTF-8 / UTF-16 / UTF-32). | -| **lspkit-live** | File-watcher → debouncer → single-flight scheduler → session lifecycle. Produces values that implement `EngineApi`. Configurable debounce and quiet windows. Stable `0.0.x` until the watcher/scheduler interplay is proven. | -| **lspkit-mcp** | `rmcp` adapter. Mounts an `EngineApi` impl as MCP tools, resources, and prompts. Provides hooks for tool descriptions that encourage prevention-first agent behavior; the descriptions themselves are consumer-supplied. | -| **lspkit-config** | Layered TOML loader. Walks up from a workspace root, merges LSP `initializationOptions`, exposes an optional hot-reload signal. Generic over the consumer's `serde::Deserialize` config struct. | -| **lspkit-treesitter** | `LanguageParser` trait, content-hash-keyed parse cache, incremental parse helper. Bundles no grammars. Optional dependency cone. | -| **lspkit-sidecar** | Framed IPC primitives only: length-prefixed envelope over Unix-socket / Windows-named-pipe, pluggable wire format, lifecycle manager (spawn, health-ping with timeout, exponential-backoff respawn, request correlation via `AtomicU64` ID). No backend-specific code of any kind. | +| **lspkit** | Façade. Exposes `EngineApi` trait + `Generation`, `Snapshot`, `GenerationEvent`, `Cause`, `RescanScope`, `RescanTicket` types. Feature `tracing-setup` exposes a stdio-safe `tracing-subscriber` builder. `prelude` module re-exports the headline items. Modules `config` (layered TOML loader), `vfs` (concurrent open-document store), `sidecar` (framed IPC transport) and `treesitter` (parse cache, behind the `treesitter` feature). | +| **lspkit-server** | LSP server scaffolding built on `lsp-types`: Content-Length framing, JSON-RPC request/response/notification dispatcher, capability builder, URI ↔ `PathBuf` helpers, workspace-folders abstraction, diagnostics fan-out bus, progress sink, cancellation plumbing. Consumers register handlers; the crate routes them to an `EngineApi` + VFS pair. Modules `live` (file-watcher → debouncer → single-flight scheduler → session lifecycle) and `mcp` (`rmcp` adapter, behind the `mcp` feature). | +| **lsp-profiler** | Language-neutral CPU and memory profiler core: sessions, hotspots, flame graphs, speedscope/V8 exports, heap snapshots, leak scoring, one JSON-RPC protocol. Spec: [LSP-PROFILER-SPEC.md](LSP-PROFILER-SPEC.md). | +| **lsp-profiler-dotnet** | The profiler's .NET runtime: EventPipe CPU sampling and heap snapshots through the engine in `dotnet/`. | -The profiler crates, `lsp-profiler` (core) and `lsp-profiler-dotnet` (the .NET runtime), sit beside these eight with their own spec: [LSP-PROFILER-SPEC.md](LSP-PROFILER-SPEC.md). +## Crate layout + +Implementation tracker: [docs/plans/crate-consolidation-plan.md](../plans/crate-consolidation-plan.md). The layer graph is under [Dependency direction](#dependency-direction-acyclic). + +- **[CRATE-LAYOUT]** The workspace publishes exactly four crates in dependency order: `lsp-profiler`, `lsp-profiler-dotnet`, `lspkit`, `lspkit-server`. `lsp-profiler-python` is parked: it stays in the repo, excluded from the workspace and unpublished. +- **[CRATE-LAYOUT-LAYERS]** An L1 crate depends only on its own tree's L0 crate. An L0 crate has no internal dependencies. +- **[CRATE-LAYOUT-MODULES]** Each absorbed crate becomes a top-level module with the same name: `lspkit::config`, `lspkit::vfs`, `lspkit::treesitter`, `lspkit::sidecar`, `lspkit_server::live`, `lspkit_server::mcp`. Each module keeps its absorbed crate's coverage floor as a `::` key in `coverage-thresholds.json`. +- **[CRATE-LAYOUT-FEATURES]** Heavy dependency cones are opt-in cargo features, all default off: `lspkit-server/mcp` (`rmcp`), `lspkit-server/pull` (`salsa`), `lspkit/treesitter` (`tree-sitter`, a C runtime) and `lspkit/tracing-setup` (`tracing-subscriber`). `lspkit::sidecar` is always on; its dependencies are already in `lspkit`'s core. +- **[CRATE-LAYOUT-HELPER]** `lsp-profiler-helper` is a `[[bin]]` target of `lsp-profiler-python` (`src/bin/lsp-profiler-helper/`), not a crate. +- **[CRATE-RETIRE]** The six absorbed `lspkit-*` names are already on crates.io at 0.0.1. After the consolidated release ships, delete `lspkit-config`, `lspkit-treesitter`, `lspkit-sidecar`, `lspkit-mcp` and `lspkit-live` on crates.io, and yank `lspkit-vfs` 0.0.1 with a deprecation README pointing to `lspkit::vfs`. ## Dependency direction (acyclic) ```mermaid flowchart TD - lspkit["lspkit
hosts EngineApi"] - lspkit-server["lspkit-server"] - lspkit-mcp["lspkit-mcp
rmcp adapter"] - lspkit-live["lspkit-live
notify + sched"] - lspkit-config["lspkit-config"] - lspkit-vfs["lspkit-vfs"] - lspkit-treesitter["lspkit-treesitter
optional cone"] - lspkit-sidecar["lspkit-sidecar
standalone"] - - lspkit-server --> lspkit - lspkit-server --> lspkit-vfs - lspkit-mcp --> lspkit - lspkit-live --> lspkit - lspkit-config - lspkit-treesitter - lspkit-sidecar + lspkit-server["lspkit-server (L1)"] --> lspkit["lspkit (L0)"] + lsp-profiler-dotnet["lsp-profiler-dotnet (L1)"] --> lsp-profiler["lsp-profiler (L0)"] + lsp-profiler-python["lsp-profiler-python (L1, parked)"] --> lsp-profiler ``` ## `EngineApi` contract @@ -98,7 +90,7 @@ pub trait EngineApi: Send + Sync + 'static { ## Cross-cutting helpers — required behavior -- **Position encoding** (`lspkit-vfs::PositionEncoding`): negotiates encoding at `initialize` per LSP 3.17 client capabilities; converts on every read; round-trips byte/char offsets correctly across multi-byte scalars and surrogate pairs. +- **Position encoding** (`lspkit::vfs::PositionEncoding`): negotiates encoding at `initialize` per LSP 3.17 client capabilities; converts on every read; round-trips byte/char offsets correctly across multi-byte scalars and surrogate pairs. - **URI ↔ `PathBuf`** (`lspkit-server::uri`): handles `file://`, percent-decoding, Windows drive letters, UNC paths, and non-UTF-8 paths. - **Cancellation** (`lspkit-server::dispatch`): a `CancellationToken` is plumbed through every request handler and triggered when the client sends `$/cancelRequest`. - **Progress** (`lspkit-server::progress`): wraps LSP `window/workDoneProgress`; the `Progress` sink is a no-op when the client did not advertise the capability. @@ -111,7 +103,7 @@ pub trait EngineApi: Send + Sync + 'static { - `lsp-types` (current major). - `rmcp` (current minor, pinned). - `notify` 7.x, `ropey` 1.x, `dashmap` 6.x, `tokio-util` (for `CancellationToken`). -- `rmp-serde` available as the default wire format for `lspkit-sidecar`; transport is generic over `serde`. +- `rmp-serde` available as the default wire format for `lspkit::sidecar`; transport is generic over `serde`. - **No `tower-lsp`.** Ever. ## Public API hygiene @@ -124,25 +116,34 @@ pub trait EngineApi: Send + Sync + 'static { ## Release & publishing -The toolkit ships eight crates to crates.io as a single coordinated release. Rules are non-negotiable; spec IDs below are referenced by the release workflow and the implementation plan in [docs/plans/publishing-plan.md](../plans/publishing-plan.md). - -- **[REL-TRIGGER]** Releases are triggered by an annotated tag matching `v[0-9]+.[0-9]+.[0-9]+` (or `v*.*.*-*` for prereleases) pushed to `main`. No `workflow_dispatch`. No publishing from feature branches. -- **[REL-OIDC]** Publishing to crates.io uses OIDC trusted publishing via `rust-lang/crates-io-auth-action`. No `CARGO_REGISTRY_TOKEN` secret is stored in the repo. Each of the eight crates registers `Nimblesite/lspkit` + workflow `.github/workflows/release.yml` + environment `release` as a Trusted Publisher on crates.io. -- **[REL-VERSION]** A single workspace version is the source of truth. `[workspace.package].version` carries the placeholder `0.0.0-dev` in source; the release workflow stamps the tag-derived version into `Cargo.toml` (and the internal `[workspace.dependencies]` pins for `lspkit-*`) before build. All eight crates inherit via `version.workspace = true`. The stamp is never committed back to `main`. -- **[REL-ORDER]** Publish order respects the dep graph above; one 30-second sleep between batches lets the crates.io index propagate: - 1. **Batch 1 (leaves):** `lspkit`, `lspkit-vfs`, `lspkit-config`, `lspkit-sidecar`, `lspkit-treesitter` - 2. **Batch 2 (dependents):** `lspkit-server`, `lspkit-mcp`, `lspkit-live` - - `cargo publish -p ` per crate; the publish step is idempotent (skip if the version is already on the index). -- **[REL-GATES]** Pre-publish gates — all must pass, in order: - 1. `make ci` (lint + test + coverage thresholds + build) green on the tagged commit. - 2. `cargo publish --dry-run -p ` succeeds for each crate. - 3. `cargo deny check` clean against committed `deny.toml`. - 4. Every `uses:` in the release workflow pinned to a 40-char commit SHA (Dependabot tracks). - 5. Top-level workflow `permissions: contents: read`; `id-token: write` job-scoped to the publish job only. - 6. `--locked` on every cargo invocation in the release workflow; `Cargo.lock` committed. -- **[REL-METADATA]** Every published crate carries `license = "MIT"`, `repository`, `description`, `readme`, and `documentation` fields. A single `LICENSE-MIT` file ships in each crate, staged from the workspace-root `LICENSE-MIT` at publish time. The declared SPDX license matches the one license file that actually ships. `[SWR-REL-LICENSE]` -- **[REL-PROVENANCE]** GitHub Release artifacts (if any) carry `actions/attest-build-provenance` attestations. Provenance for crates.io packages is roadmap; not blocking for `0.0.x`. +lspkit runs a **release-only** regime modelled on [Shipwright](https://github.com/Nimblesite/Shipwright/tree/main/docs/specs) and the AgentPMO repo standard. The one way to ship is to push a version tag: [.github/workflows/release.yml](../../.github/workflows/release.yml) gates the tagged commit, cuts a signed GitHub Release and publishes the four crates to crates.io and every package under `packages/` to npm. Spec IDs below are cited by the workflows and by the plan in [docs/plans/publishing-plan.md](../plans/publishing-plan.md). + +- **[REL-REGIME]** Workflow triggers are fixed: + + | Workflow | Trigger | Purpose | + |---|---|---| + | `ci.yml` | PR to `main` | `make ci` gate | + | `supply-chain.yml` | PR to `main`, weekly cron | cargo-deny + osv-scanner | + | `codeql.yml` | PR to `main`, weekly cron, `workflow_call` from release | CodeQL scan; a hard gate when called by release | + | `release.yml` | push of a `v*` tag | gate, GitHub Release, crates.io, npm | + | `dependabot-automerge.yml` | Dependabot PR | sweep the bump into `dependabot-upgrades` | + + A push or merge to `main` triggers nothing. Nothing publishes on a schedule, from a branch or from `workflow_dispatch`. +- **[REL-TRIGGER]** A release starts when a tag matching `v[0-9]+.[0-9]+.[0-9]+` (or `v[0-9]+.[0-9]+.[0-9]+-*` for a prerelease) is pushed at a commit on `main`. Publishing a GitHub Release from the UI or with `gh release create vX.Y.Z` creates that tag and so starts the same workflow; the workflow then fills in that release instead of creating a new one. +- **[REL-VERSION]** Every source version is the placeholder `0.0.0-dev`: `[workspace.package].version`, the internal `[workspace.dependencies]` pins and each `packages/*/package.json`. The release stamps the tag version into every carrier with `shipwright-version-stamp` in the runner working tree, and never commits it back. `[SWR-VERSION-BUILD-STAMPING]` +- **[REL-GATES]** Before anything publishes, all of these pass on the tagged commit, in order: + 1. The CodeQL gate (`codeql.yml` with `gate: true`) finds nothing at High or Critical severity. + 2. `make ci` (format, lint, test, coverage thresholds, build). + 3. `cargo deny check advisories bans licenses sources` against the stamped, relocked graph. + 4. `cargo package --workspace --locked` builds every crate tarball against its siblings. + + Every `uses:` is pinned to a 40-character commit SHA. Workflows default to `permissions: contents: read` and elevate per job. Installs are frozen (`npm ci`, `--locked`), and the release job uses no caches. +- **[REL-GITHUB]** The release attaches every crate tarball, every npm tarball, a CycloneDX SBOM and one `SHA256SUMS` over them all, cosign keyless-signed (`SHA256SUMS.sigstore.json`). Build provenance and the SBOM are attested against `SHA256SUMS`, so consumers can run `gh attestation verify --repo Nimblesite/lspkit`. `[SWR-SEC-CHECKSUM]` `[SWR-SEC-PROVENANCE]` `[SWR-SEC-SBOM]` +- **[REL-OIDC]** crates.io and npm both publish through OIDC Trusted Publishing from `release.yml` in the protected `release` environment. The repo stores no `CARGO_REGISTRY_TOKEN` or `NPM_TOKEN`. npm packages also publish with `--provenance`. `[SWR-SEC-OIDC-PUBLISH]` +- **[REL-ORDER]** Crates publish in dependency order: `lsp-profiler`, `lsp-profiler-dotnet`, `lspkit`, `lspkit-server`. `cargo publish` waits for each crate to appear in the index, so no sleeps are needed. The npm package (`@nimblesite/lsp-profiler-ui`) publishes after the crates. Both publish wrappers skip a version that is already on the registry, so a re-run after a failure partway through is safe. +- **[REL-METADATA]** Every published crate and npm package declares `license = "MIT"` and a repository. The root `LICENSE-MIT` is staged into each crate (`LICENSE-MIT`) and each npm package (`LICENSE`) at publish time, so the declared license ships with the package. `[SWR-REL-LICENSE]` +- **[REL-PACKAGED-BUILD]** A published crate builds without the workspace around it. `lsp-profiler`'s `build.rs` runs the typeDiagram generator only when the workspace generator exists. Otherwise it requires the generated `src/wire_generated.rs` that the crate's `include` packs. `[PROF-WIRE-PACKAGED]` +- **[REL-DEPENDABOT]** Every Dependabot bump lands on the unprotected `dependabot-upgrades` staging branch. CI, CodeQL and supply-chain skip Dependabot PRs, and the batch reaches `main` through one consolidation PR. ## Acceptance — externally verifiable @@ -156,6 +157,7 @@ A patch is acceptable iff: 6. `grep -R 'tower_lsp\|rmcp::' crates/*/src/lib.rs` returns **zero hits** in `pub` signatures (manual review against the rule). 7. The `EngineApi` trait skeleton compiles and is exercised by a smoke test that constructs a no-op implementation and calls each method. 8. Each of the four source repos has a "Migration to `lspkit`" section in its AI-guidance file. -9. **[REL-OIDC]** `.github/workflows/release.yml` exists, uses `rust-lang/crates-io-auth-action` under `environment: release`, declares no `CARGO_REGISTRY_TOKEN` secret, and pins every `uses:` to a 40-char SHA. -10. **[REL-VERSION]** `[workspace.package].version = "0.0.0-dev"` in source; release workflow stamps from `${{ github.ref_name }}`; every crate uses `version.workspace = true`. -11. **[REL-ORDER]** Release workflow publishes the eight crates in the two batches above with a sleep between, and uses an idempotent skip-if-already-published wrapper. +9. **[REL-OIDC]** `.github/workflows/release.yml` publishes crates.io (`rust-lang/crates-io-auth-action`) and npm (`--provenance`) under `environment: release`, references no registry token secret, and pins every `uses:` to a 40-char SHA. +10. **[REL-VERSION]** Every source version is `0.0.0-dev`, and the release stamps from `${{ github.ref_name }}`. +11. **[REL-ORDER]** The release publishes all four crates and both npm packages in the order above, through the idempotent skip-if-published wrappers. +12. **[REL-REGIME]** No workflow triggers on `push` to a branch. `actionlint` and `zizmor --min-severity medium` report nothing for `.github/workflows/`. diff --git a/docs/specs/website-design-system.md b/docs/specs/website-design-system.md new file mode 100644 index 0000000..f59cc18 --- /dev/null +++ b/docs/specs/website-design-system.md @@ -0,0 +1,64 @@ +# lspkit / Fieldwork + +A compact design system for the toolkit's documentation and engineering journal. The visual idea: an architect's field notebook meets a precision instrument. Warm paper, ink, a single signal colour, and diagrams that make engineering feel tangible. + +## [WEB-DESIGN] Art direction + +**Confident, precise, open.** Oversized headlines establish a point of view; small monospaced labels supply context. Space is the primary organising device. Thin rules separate sections. Avoid decorative cards, soft shadows, gradients, pill-shaped navigation and marketing statistics. + +Use the existing `lspkit` wordmark in lowercase, accompanied by a small vermilion square. Headlines use sentence case. Copy is direct and technical, with no invented adoption claims. The homepage leads with “One engine. Every surface.” and a short explanation of the toolkit. + +### [WEB-DESIGN-COLOR] Semantic palette + +| Token | Light | Dark | Purpose | +| --- | --- | --- | --- | +| `--color-bg` | `#f4f1e9` | `#191c19` | Paper / canvas | +| `--color-text` | `#202720` | `#f4f1e9` | Ink / primary text | +| `--color-primary` | `#ad321e` | `#ff997f` | Links, focus and active navigation | +| `--color-muted` | `#60665b` | `#b3b9ac` | Metadata and captions | +| `--color-border` | `#c9ccbf` | `#454b41` | Quiet structural rules | +| `--color-panel` | `#e8ebdf` | `#252c24` | Image surround and code | + +The illustration uses a stronger vermilion `#e34b31` as an image colour, never as small text on paper. Theme controls preserve the visitor's choice using techdoc's bundled implementation. + +### [WEB-DESIGN-TYPE] Typography + +Use the local system sans stack (`Arial`, `Helvetica`, sans-serif) for fast, dependable rendering and `SFMono-Regular`, `Consolas`, monospace for labels and code. No remote font requests. + +| Role | Size | Treatment | +| --- | --- | --- | +| Hero | fluid 52–104 px | 0.96 line height, −0.065em tracking, bold | +| Page title | fluid 40–72 px | 1.05 line height, −0.05em tracking | +| Section title | fluid 28–44 px | 1.12 line height, −0.04em tracking | +| Body | 18 px | 1.7 line height, maximum 68ch | +| Label | 12 px | monospace, uppercase, 0.1em tracking | + +### [WEB-DESIGN-LAYOUT] Grid and rhythm + +Content spans at most 1200 px, with 32 px desktop gutters and 20 px mobile gutters. The spacing scale is 4, 8, 12, 16, 24, 32, 48, 64 and 96 px. The hero is a wide text column plus a narrow explanatory column; below 768 px, it becomes one column. Images remain proportional. Use square corners and 1 px dividers. + +### [WEB-DESIGN-COMPONENTS] Components + +- **Masthead:** small wordmark, Docs / Journal navigation, an outlined theme control. Active links use an underline as well as colour. +- **Hero:** eyebrow, large two-line title, short introduction and underlined text action. No unnecessary call-to-action stack. +- **Header artwork:** an original SVG engineering plate: one central engine routes into two terminal surfaces. Fine grid, dark outlines, vermilion core. Decorative labels belong in the surrounding HTML so they remain readable. +- **Journal row:** issue number, title, excerpt, publication date and arrow. One real example post, sourced from the plugin's collection. +- **Article:** metadata, large heading, full-width header image, narrow body and a clear back link. Use techdoc's shared blog layout. +- **Code:** quiet panel with overflow scrolling; inherit syntax highlighting from techdoc with accessible token colours. +- **Footer:** compact project identity and links to the repository and Atom feed. + +## [WEB-CONTENT] Minimal information architecture + +`/` introduces the toolkit and features the example post. `/docs/` explains the three architectural basics. `/blog/` and the tag/category pages are generated by techdoc. `/blog/one-engine/` contains one short example post with the header image. The plugin also owns the feed, sitemap and structural assets; do not copy its templates into this project. + +The canonical origin defaults to `http://localhost:8080` for this local sketch and is configured with `SITE_URL` before a production build. Do not invent a live domain or publish through a new workflow. + +## [WEB-ACCESS] Interaction and acceptance + +All actions are real links or buttons. Keep the plugin's skip link, semantic headings and keyboard-operable navigation. Focus uses a visible 3 px outline with an offset. Controls have a minimum 44 px target. Links in prose are underlined. Images have meaningful alternatives; the diagram is also described in the article text. No animation is required. + +Check the homepage, docs, blog and article at desktop and 390 px mobile widths, with both colour themes and the mobile menu. Confirm no horizontal overflow, no broken local navigation or assets, valid generated structured data, and a successful Eleventy production build. The design is a starter, not a claim of a full accessibility audit. + +## Implementation map + +`website/src/assets/css/styles.css` implements the visual tokens and components. `website/eleventy.config.js` registers `eleventy-plugin-techdoc`; `website/src/_data/` supplies shared metadata. The image is `website/src/assets/images/engine.svg`. Track completion in [the website plan](../plans/website-plan.md). diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index f9650a8..fb9748e 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -5,8 +5,10 @@ enable enable true - latest-recommended + latest-all true + + $(WarnOn);1182;3390 true portable diff --git a/dotnet/LspKit.Sidecar/RequestPump.cs b/dotnet/LspKit.Sidecar/RequestPump.cs index 3d6445a..c8f1fe3 100644 --- a/dotnet/LspKit.Sidecar/RequestPump.cs +++ b/dotnet/LspKit.Sidecar/RequestPump.cs @@ -64,7 +64,8 @@ public WorkResult Cancel(uint id) { return new WorkResult.Ok(false); } - catch (Exception error) + // A cancellation callback threw; Cancel aggregates every callback failure. + catch (AggregateException error) { return WorkResult.Failure(error.Message); } @@ -80,7 +81,8 @@ private async Task RunAsync() { return new WorkResult.Ok(false); } - catch (Exception error) + // The worker boundary: any failure becomes a result, except the runtime's own fatal states. + catch (Exception error) when (error is not (OutOfMemoryException or StackOverflowException)) { return WorkResult.Failure(error.Message); } @@ -106,7 +108,8 @@ private async Task InvokeAsync(Call call) { return await _handle(call.Request, call.Source.Token).ConfigureAwait(false); } - catch (Exception error) + // A handler failure is the request's failure response, never the pump's. + catch (Exception error) when (error is not (OutOfMemoryException or StackOverflowException)) { return _failure(call.Request, error); } diff --git a/dotnet/LspProfiler.DotNet.Engine/SourceLocator.cs b/dotnet/LspProfiler.DotNet.Engine/SourceLocator.cs index 80df4f3..d9be268 100644 --- a/dotnet/LspProfiler.DotNet.Engine/SourceLocator.cs +++ b/dotnet/LspProfiler.DotNet.Engine/SourceLocator.cs @@ -73,19 +73,26 @@ private sealed class ModuleSymbols(PEReader pe, MetadataReaderProvider pdbProvid public static ModuleSymbols? Open(string assembly) { - var pe = new PEReader(File.OpenRead(assembly)); + // Ownership moves to the ModuleSymbols on success; every other path disposes both readers. + PEReader? pe = new(File.OpenRead(assembly)); + MetadataReaderProvider? provider = null; try { - var found = pe.TryOpenAssociatedPortablePdb(assembly, OpenIfPresent, out var provider, out _); - if (found && provider is not null) + if (pe.TryOpenAssociatedPortablePdb(assembly, OpenIfPresent, out provider, out _) && provider is not null) { - return new ModuleSymbols(pe, provider); + var symbols = new ModuleSymbols(pe, provider); + (pe, provider) = (null, null); + return symbols; } } catch (Exception error) when (error is BadImageFormatException or IOException or InvalidOperationException) { } - pe.Dispose(); + finally + { + provider?.Dispose(); + pe?.Dispose(); + } return null; } diff --git a/packages/.oxlintrc.json b/packages/.oxlintrc.json new file mode 100644 index 0000000..bef3efd --- /dev/null +++ b/packages/.oxlintrc.json @@ -0,0 +1,86 @@ +{ + // The one lint config for every TypeScript package in lspkit. Every category is + // an error. A rule is off or reshaped only with the reason beside it. + "$schema": "./lsp-profiler-ui/node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "promise", "node"], + "categories": { + "correctness": "error", + "suspicious": "error", + "pedantic": "error", + "perf": "error", + "style": "error", + "restriction": "error" + }, + "env": { "node": true, "es2024": true }, + "rules": { + // ES5-compatibility restrictions; TypeScript 7 targets ES2022, where these are the idiom. + "oxc/no-async-await": "off", + "oxc/no-optional-chaining": "off", + "oxc/no-rest-spread-properties": "off", + // The package compiles to CommonJS for the VS Code extension host, where `__dirname` is the API. + "unicorn/prefer-module": "off", + // Named exports are the module contract; default exports stay banned. + "import/no-named-export": "off", + "import/prefer-default-export": "off", + // Removing `undefined` from a call breaks parameters that are required but nullable. + "unicorn/no-useless-undefined": ["error", { "checkArguments": false }], + // Contradict co-locating each export with its declaration. + "import/group-exports": "off", + "import/exports-last": "off", + // `import * as vscode` is the VS Code API convention. + "import/no-namespace": "off", + // This package runs on Node and in the VS Code extension host. + "import/no-nodejs-modules": "off", + // Tests and shared modules import from parent directories by design. + "import/no-relative-parent-imports": "off", + // Contradicts unicorn/no-null: VS Code APIs and optional fields are `undefined`. + "eslint/no-undefined": "off", + // Ternaries are how "expressions over statements" is written in TypeScript. + "eslint/no-ternary": "off", + // Object key order is meaningful in rendered UI and wire payloads. + "eslint/sort-keys": "off", + // Deep-readonly parameters are impossible for VS Code API types. + "typescript/prefer-readonly-parameter-types": "off", + // Fights typescript/promise-function-async; typescript/require-await is the type-aware check. + "eslint/require-await": "off", + // LSP and DAP wire values use null; `=== null` checks on them are correct. + "unicorn/no-null": "off", + // Declaration order is grouped by meaning; members within an import stay sorted. + "eslint/sort-imports": ["error", { "ignoreDeclarationSort": true }], + // `import type` lines are split from value imports by import/consistent-type-specifier-style. + "eslint/no-duplicate-imports": ["error", { "allowSeparateTypeImports": true }], + // `void promise` is how a deliberately unawaited promise is marked. + "eslint/no-void": ["error", { "allowAsStatement": true }], + // Wrapped comments continue mid-sentence on the next line. + "eslint/capitalized-comments": ["error", "always", { "ignoreConsecutiveComments": true }], + "eslint/func-style": ["error", "declaration", { "allowArrowFunctions": true }], + "eslint/one-var": ["error", "never"], + "eslint/no-use-before-define": ["error", { "functions": false }], + "eslint/id-length": ["error", { "min": 2, "exceptions": ["_", "i", "j", "x", "y"] }], + // Enum members must be literals (typescript/prefer-literal-enum-member), so enums are exempt. + "eslint/no-magic-numbers": ["error", { "ignore": [-1, 0, 1, 2], "ignoreArrayIndexes": true, "ignoreDefaultValues": true, "ignoreEnums": true }], + // VS Code interface implementations (TreeDataProvider, FileDecorationProvider) need instance methods. + "eslint/class-methods-use-this": ["error", { "ignoreClassesWithImplements": "all" }], + // VS Code's Webview.postMessage and acquireVsCodeApi().postMessage take one argument; there is no targetOrigin. + "unicorn/require-post-message-target-origin": "off", + // AGENTS.md: functions < 20 lines, files < 500 lines. + "eslint/max-lines-per-function": ["error", { "max": 20, "skipBlankLines": true, "skipComments": true }], + "eslint/max-lines": ["error", { "max": 500, "skipBlankLines": true, "skipComments": true }] + }, + "overrides": [ + { + // Long user-interaction tests with many assertions are the house style. + "files": ["**/test/**/*.ts", "**/*.test.ts", "**/src/**/testing/**/*.ts"], + "rules": { + "eslint/max-lines-per-function": "off", + "eslint/max-statements": "off", + "eslint/no-magic-numbers": "off", + // Test doubles implement async interfaces without needing to await anything. + "typescript/require-await": "off", + // Shared contract suites (e.g. testing/webview-contract) register their tests on import. + "import/no-unassigned-import": "off" + } + } + ], + "ignorePatterns": ["**/out/**", "**/dist/**", "**/coverage/**", "**/node_modules/**", "**/.vscode-test/**", "**/wire-generated.ts"] +} diff --git a/packages/lspkit-debug/.c8rc.json b/packages/lsp-profiler-ui/.c8rc.json similarity index 59% rename from packages/lspkit-debug/.c8rc.json rename to packages/lsp-profiler-ui/.c8rc.json index 8ff1eca..652dc89 100644 --- a/packages/lspkit-debug/.c8rc.json +++ b/packages/lsp-profiler-ui/.c8rc.json @@ -1,7 +1,7 @@ { "all": true, - "include": ["out/src/**/*.js"], - "exclude": ["**/index.js", "**/index.ts", "**/*.d.ts", "out/test/**", "test/**"], + "include": ["out/src/debug/**/*.js"], + "exclude": ["**/index.js", "**/*.d.ts", "out/src/debug/test/**"], "reporter": ["text", "text-summary", "json-summary"], "check-coverage": true, "branches": 90, diff --git a/packages/lsp-profiler-ui/eslint.config.mjs b/packages/lsp-profiler-ui/eslint.config.mjs deleted file mode 100644 index d806cc0..0000000 --- a/packages/lsp-profiler-ui/eslint.config.mjs +++ /dev/null @@ -1,199 +0,0 @@ -// The UI package's static gate: SharpLsp's extension rules, unchanged, so a -// screen moved here from a host is held to the bar it was written to [PROF-UI]. -import tseslint from 'typescript-eslint'; -import eslint from '@eslint/js'; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.strictTypeChecked, - ...tseslint.configs.stylisticTypeChecked, - { - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - // Source files — max type safety - files: ['src/**/*.ts'], - ignores: ['src/test/**/*.ts'], - rules: { - // ── 1. No implicit any — every value must have a known type ──── - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unsafe-assignment': 'error', - '@typescript-eslint/no-unsafe-call': 'error', - '@typescript-eslint/no-unsafe-member-access': 'error', - '@typescript-eslint/no-unsafe-return': 'error', - '@typescript-eslint/no-unsafe-argument': 'error', - - // ── 2. Strict null checks — no accidental undefined ──────────── - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/strict-boolean-expressions': [ - 'error', - { - allowNullableBoolean: false, - allowNullableString: false, - allowNullableNumber: false, - allowNullableObject: false, - allowAny: false, - }, - ], - - // ── 3. Exhaustive switches — catch missing enum cases ────────── - '@typescript-eslint/switch-exhaustiveness-check': 'error', - - // ── 4. No floating promises — every async call awaited ───────── - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-promises': 'error', - 'no-void': ['error', { allowAsStatement: true }], - - // ── 5. No unused variables — dead code is a bug vector ───────── - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - }, - ], - - // ── 6. Prefer const — immutability by default ────────────────── - 'prefer-const': 'error', - - // ── 7. Explicit return types — contracts must be clear ───────── - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: true, - allowTypedFunctionExpressions: true, - allowHigherOrderFunctions: true, - }, - ], - - // ── 8. No shadow — inner vars must not hide outer vars ───────── - '@typescript-eslint/no-shadow': 'error', - 'no-shadow': 'off', - - // ── 9. Consistent type imports — enforce `import type` ───────── - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - prefer: 'type-imports', - fixStyle: 'inline-type-imports', - }, - ], - - // ── 10. No require — ESM only ───────────────────────────────── - '@typescript-eslint/no-require-imports': 'error', - - // ── 11. Immutable fields — never-reassigned privates must be readonly ─ - '@typescript-eslint/prefer-readonly': 'error', - - // ── 12. Safe sort — Array.sort() without comparator is a bug ──────── - '@typescript-eslint/require-array-sort-compare': 'error', - - // ── 13. Async consistency — promise-returning functions must be async ─ - '@typescript-eslint/promise-function-async': 'error', - - // ── 14. Explicit visibility — no implicit public on class members ──── - '@typescript-eslint/explicit-member-accessibility': [ - 'error', - { - accessibility: 'explicit', - overrides: { constructors: 'no-public' }, - }, - ], - - // ── 15. No deprecated — flag usage of deprecated APIs immediately ──── - '@typescript-eslint/no-deprecated': 'error', - - // ── 16. No type assertions — casting is illegal ───────────────── - '@typescript-eslint/consistent-type-assertions': [ - 'error', - { - assertionStyle: 'never', - }, - ], - - // Conflicts with no-non-null-assertion — disable the weaker rule. - '@typescript-eslint/non-nullable-type-assertion-style': 'off', - - // ── 17. No raw console — diagnostics go through the `logger` module ── - // Source logs through the host-bound `./logger`; test files keep - // console access via the relaxed test override below. - 'no-console': 'error', - - // ── 18. Await before returning — a bare `return promise` inside a - // try block escapes its own catch/finally, so the error surfaces - // as an unhandled rejection instead of being handled, and the - // stack trace loses every frame above the async boundary. ─────── - '@typescript-eslint/return-await': ['error', 'always'], - 'no-return-await': 'off', - - // ── 19. No async races — reading a shared variable, awaiting, then - // assigning back lets a second call interleave and clobber the - // result. The UI caches sessions, panels and results across - // awaits, which is exactly where this bites. ─────────────────── - 'require-atomic-updates': 'error', - - // ── 20. A value returned from a Promise executor is discarded, always. - // It reads like it resolves the promise and never does, so the - // promise hangs forever — the loopback profile server builds - // promises around listen callbacks. ─────────────────────────── - 'no-promise-executor-return': 'error', - - // ── Bonus rules ──────────────────────────────────────────────── - eqeqeq: ['error', 'always'], - 'no-param-reassign': 'error', - '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - }, - }, - { - // Test files — relaxed - files: ['src/test/**/*.ts'], - rules: { - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/strict-boolean-expressions': 'off', - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }], - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/require-await': 'off', - '@typescript-eslint/dot-notation': 'off', - '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', - '@typescript-eslint/no-unnecessary-condition': 'off', - '@typescript-eslint/no-base-to-string': 'off', - '@typescript-eslint/unbound-method': 'off', - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - }, - }, - { - ignores: [ - 'out/', - 'dist/', - 'node_modules/', - // The VS Code build the test runner downloads: not our source. - '.vscode-test/', - // Generated by typeDiagram from docs/models/profiler.td [PROF-WIRE-GEN]. - 'src/wire-generated.ts', - '**/*.mjs', - '**/*.cjs', - '**/*.js', - ], - }, -); diff --git a/packages/lsp-profiler-ui/package-lock.json b/packages/lsp-profiler-ui/package-lock.json index f6eeaa1..ad94764 100644 --- a/packages/lsp-profiler-ui/package-lock.json +++ b/packages/lsp-profiler-ui/package-lock.json @@ -1,28 +1,28 @@ { "name": "@nimblesite/lsp-profiler-ui", - "version": "0.0.1", + "version": "0.0.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nimblesite/lsp-profiler-ui", - "version": "0.0.1", + "version": "0.0.0-dev", "license": "MIT", "dependencies": { "@preact/signals-core": "^1.14.4" }, "devDependencies": { - "@eslint/js": "^10.0.1", "@types/mocha": "^10.0.10", "@types/node": "^26.1.2", "@types/vscode": "^1.99.0", "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^3.1.0", - "eslint": "^10.13.0", - "glob": "^13.0.6", + "c8": "^12.0.0", + "glob": "^14.0.3", "mocha": "^12.0.3", - "typescript": "~6.0.3", - "typescript-eslint": "^8.72.0", + "oxlint": "^1.90.0", + "oxlint-tsgolint": "^7.0.2005", + "typescript": "^7.0.2", "vscode-languageclient": "^10.1.0", "vscode-languageserver": "^10.0.0", "vscode-languageserver-textdocument": "^1.0.15" @@ -44,285 +44,457 @@ "node": ">=18" } }, - "node_modules/@cacheable/memory": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", - "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.5.0", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" + "engines": { + "node": ">=8" } }, - "node_modules/@cacheable/utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", - "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz", + "integrity": "sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz", + "integrity": "sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz", + "integrity": "sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz", + "integrity": "sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz", + "integrity": "sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz", + "integrity": "sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.90.0.tgz", + "integrity": "sha512-sgfalb1HuxYjiAa4wESZ626z1FGusqNcXYHyB5oIOEprS2sbPg0UYEfo83uF1GNG70DIfZxHkXuE6KFrlSdn8Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.90.0.tgz", + "integrity": "sha512-RcKhz1ETMq+fazE9TKS2+5NIRGwigm/nW8egqF0LI+A8Yw2iW5r0E/C3XAPRiwIm/vcPiutIRIa6IQCIKhZsTQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.90.0.tgz", + "integrity": "sha512-PSrxMvP6mx+p1karZcUxLdy9lpaP+JWQPti4al9FgTaPhxE+pT/BjhGExloVVSWgZgIfOlLOjVyyq8BXlodhLQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.90.0.tgz", + "integrity": "sha512-OLbU9qsrV2Q8QxT2OEzOPdO8zCCpyI8qB+hDI8yr6RsYELtkf0FAZhJdfjM/RnLHKzt44HWwAPnk92vBtKofvw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.90.0.tgz", + "integrity": "sha512-fBA4pWyVDARmNucQqpyFuPC49xYf1r+sX7dxPL5fNPQrSMLum39Sn56tZ9JPleWC7FzLPwSQ7plYYfirYBHF8A==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.90.0.tgz", + "integrity": "sha512-zhxH78eP2nR6/pGdhBc7MIfjhhcgATeu8fkmkFJbuzfk6Cj0FzBxyEhL85yDqrCybjx+G2YDzZGsxIO9kvO4Iw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.90.0.tgz", + "integrity": "sha512-MvYmpHyJqUJjEvkZbJHdwfdL45cGn8EVBkYmoXFiaYFW9suoD3dbJl5mBdpy0DVgdjlyddmldbRtk3cCgETadQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", - "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.90.0.tgz", + "integrity": "sha512-L13Sy+Kg0nZhwTsToniqjUFPA2TuM6962qCZ5Wp5KeB517Tw7ApdLTTtlb+bF86fcZZa2EtvCVvAJxk4Dya/MQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.90.0.tgz", + "integrity": "sha512-wFDdZWGpzIPFdJFjSLWnKB0TGzFriTKUIrl04iMqMyM9BBIJTuerfCHYa0WpDSjr5x3lcW/CHLyacSt886WpGg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.90.0.tgz", + "integrity": "sha512-FRywfn+MoKn8xpBSzgNwNVliSKRI2QXkxi/iF7o6FEQs8Bh8z2RpVTaFAcO3pchXjvB2eGNFIoLLQABIQ+u7VA==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.90.0.tgz", + "integrity": "sha512-IS2PaUgpc/qeLADTyhUNmdx3x4ijs9pjJdPKBYHaT+mAE7k3FsjnHPTjhpsak1b++h6CIPhv6WX9Z+y7Pw0voQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.90.0.tgz", + "integrity": "sha512-s7JFKRkGS8O7i2ZVODfuO++NC7sH6HDqpMn8lYxw91w0SXJOxnAVjQ8utvmDwLMMT0jaSkuOKuUYJDGDtu4sHQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.90.0.tgz", + "integrity": "sha512-b0GxrOQr5VONCkd7KLSqmP6b6nqZuTpuS8QPQHzltmqdXP5bY0Jl0CZ0QmlCTrS3ExaLYbioFYOARzNbYvcApQ==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.90.0.tgz", + "integrity": "sha512-iaJeShJtgUp8rZbt9YUdTAGOpmX5KogCLba17Jp1tWdJ3F4M3D2g2TKUyI1FMTHxKH9NNHuQcuC2kZRg6IV7yQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.90.0.tgz", + "integrity": "sha512-/aZoXnL5WBLCXuzfHqvEFXcm4iwYmr/cYByHDTXzLFzJdE5DC7bm6phpWqHv6BeqpVgY9IpBAxGDv2WEkl5JCg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.90.0.tgz", + "integrity": "sha512-SMhk7ozwoxYDKfYqGZCQEfULOKDneF9uhaoIf2neoDLWvNtjY9yVeL7YHmE5L9WdKvMpWNwwZxhenxBu6AgfOg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.90.0.tgz", + "integrity": "sha512-KBvR3A9fO7fCi2vCP8ifWvlia7tGEQV9NaYh+IGwa/7Groy48Mt/UaYqMK9/zOtAWl20BUCf6RPqzJ32fRw33Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.90.0.tgz", + "integrity": "sha512-+x42rDHQGbTCuT3EzpWAzhP3NoVPE/cJsG4gnrakOSg0olV+GCdMpPU23Nvrl3zZMnXxFZdTtDv4D3SuaA5ZeQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.90.0.tgz", + "integrity": "sha512-30JiaFEBUB9peEOcvVM8ggo88SoN/IdSr9DW505JdBji/R6dM66gKEiG65hw6qcaF2f4mO0cEJ+RwRZprmun9g==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, "node_modules/@preact/signals-core": { "version": "1.14.4", @@ -334,20 +506,6 @@ "url": "https://opencollective.com/preact" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -355,13 +513,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", @@ -386,258 +537,515 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", - "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/type-utils": "8.72.0", - "@typescript-eslint/utils": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.72.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", - "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 4" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", - "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", - "debug": "^4.4.3" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", - "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.72.0", - "@typescript-eslint/types": "^8.72.0", - "debug": "^4.4.3" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", - "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", - "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", - "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.15.tgz", + "integrity": "sha512-nAxk2X79wuXS7aOhyFFhFcCqd7EBUoMesu7ZgsYE/4eFjyBMuyIweVE94BxdKH1RieN8eOz2SIrljrZt6Lk9fQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/utils": "8.72.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@types/mocha": "^10.0.10", + "c8": "^11.0.0", + "chokidar": "^5.0.0", + "enhanced-resolve": "^5.24.0", + "glob": "^13.0.6", + "minimatch": "^10.2.5", + "mocha": "^11.7.6", + "supports-color": "^10.2.2", + "yargs": "^18.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "bin": { + "vscode-test": "out/bin.mjs" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=22" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", - "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "node_modules/@vscode/test-cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=8" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", - "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "node_modules/@vscode/test-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.72.0", - "@typescript-eslint/tsconfig-utils": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "color-convert": "^2.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vscode/test-cli/node_modules/c8": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", + "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^8.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": "20 || >=22" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", - "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", + "node_modules/@vscode/test-cli/node_modules/c8/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12" + } + }, + "node_modules/@vscode/test-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=12" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", - "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "node_modules/@vscode/test-cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/test-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", - "eslint-visitor-keys": "^5.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=8" } }, - "node_modules/@vscode/test-cli": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.15.tgz", - "integrity": "sha512-nAxk2X79wuXS7aOhyFFhFcCqd7EBUoMesu7ZgsYE/4eFjyBMuyIweVE94BxdKH1RieN8eOz2SIrljrZt6Lk9fQ==", + "node_modules/@vscode/test-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "@types/mocha": "^10.0.10", - "c8": "^11.0.0", - "chokidar": "^5.0.0", - "enhanced-resolve": "^5.24.0", - "glob": "^13.0.6", - "minimatch": "^10.2.5", - "mocha": "^11.7.6", - "supports-color": "^10.2.2", - "yargs": "^18.0.0" + "ansi-regex": "^5.0.1" }, - "bin": { - "vscode-test": "out/bin.mjs" + "engines": { + "node": ">=8" + } + }, + "node_modules/@vscode/test-cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=22" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@vscode/test-electron": { @@ -657,29 +1065,6 @@ "node": ">=22" } }, - "node_modules/acorn": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", - "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -690,23 +1075,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", @@ -781,9 +1149,9 @@ "license": "ISC" }, "node_modules/c8": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", - "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", + "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", "dev": true, "license": "ISC", "dependencies": { @@ -796,14 +1164,14 @@ "istanbul-reports": "^3.1.6", "test-exclude": "^8.0.0", "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", + "yargs": "^18.0.0", "yargs-parser": "^21.1.1" }, "bin": { "c8": "bin/c8.js" }, "engines": { - "node": "20 || >=22" + "node": "^20.19.0 || ^22.12.0 || >=23" }, "peerDependencies": { "monocart-coverage-reports": "^2" @@ -814,133 +1182,6 @@ } } }, - "node_modules/c8/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/c8/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/c8/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/c8/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/c8/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/c8/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/c8/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/c8/node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cacheable": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", - "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.2.0", - "@cacheable/utils": "^2.5.0", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1046,304 +1287,80 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", - "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", - "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "11.1.5 || >11.1.6 <12", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "license": "MIT" }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=0.10" + "node": ">= 8" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=0.3.1" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=10.13.0" } }, - "node_modules/file-entry-cache": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", - "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^6.1.23" + "engines": { + "node": ">=6" } }, "node_modules/find-up": { @@ -1376,25 +1393,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "6.1.23", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", - "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cacheable": "^2.5.0", - "flatted": "^3.4.2", - "hookified": "^1.15.0" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1436,15 +1434,15 @@ } }, "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.3.tgz", + "integrity": "sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.2.2", + "minimatch": "^11.0.2", "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "path-scurry": "^3.0.0" }, "engines": { "node": "18 || 20 || >=22" @@ -1453,19 +1451,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1483,26 +1468,6 @@ "node": ">=8" } }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "dev": true, - "license": "MIT" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -1538,16 +1503,6 @@ "node": ">= 14" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -1555,16 +1510,6 @@ "dev": true, "license": "MIT" }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1572,16 +1517,6 @@ "dev": true, "license": "ISC" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1592,19 +1527,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -1733,20 +1655,6 @@ "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/jszip": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", @@ -1760,30 +1668,6 @@ "setimmediate": "^1.0.5" } }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -1880,13 +1764,14 @@ } }, "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.3.tgz", + "integrity": "sha512-iny/40PEGkNJQ5/Pv6z3nm6FbRAby32GeZRjCUJk2WnNDbgPSTcdI+DDvLqsgBdD4cD5sMtMheqPPnehCCa8Nw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.8" + "@isaacs/nonrecursive-glob-match": "^1.0.3", + "brace-expansion": "^5.0.12" }, "engines": { "node": "18 || 20 || >=22" @@ -1959,13 +1844,6 @@ "dev": true, "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -1982,24 +1860,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -2037,6 +1897,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oxlint": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.90.0.tgz", + "integrity": "sha512-RkTN9EmgIUpLuhNoLhO3OliMGJlvb3Dk7XVjBZnfoMCHrVpuz5ESd97klfiyYeV7XfA2lNLnDCPMkUI+Qw5How==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.90.0", + "@oxlint/binding-android-arm64": "1.90.0", + "@oxlint/binding-darwin-arm64": "1.90.0", + "@oxlint/binding-darwin-x64": "1.90.0", + "@oxlint/binding-freebsd-x64": "1.90.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.90.0", + "@oxlint/binding-linux-arm-musleabihf": "1.90.0", + "@oxlint/binding-linux-arm64-gnu": "1.90.0", + "@oxlint/binding-linux-arm64-musl": "1.90.0", + "@oxlint/binding-linux-ppc64-gnu": "1.90.0", + "@oxlint/binding-linux-riscv64-gnu": "1.90.0", + "@oxlint/binding-linux-riscv64-musl": "1.90.0", + "@oxlint/binding-linux-s390x-gnu": "1.90.0", + "@oxlint/binding-linux-x64-gnu": "1.90.0", + "@oxlint/binding-linux-x64-musl": "1.90.0", + "@oxlint/binding-openharmony-arm64": "1.90.0", + "@oxlint/binding-win32-arm64-msvc": "1.90.0", + "@oxlint/binding-win32-ia32-msvc": "1.90.0", + "@oxlint/binding-win32-x64-msvc": "1.90.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2005", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz", + "integrity": "sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2005", + "@oxlint-tsgolint/darwin-x64": "7.0.2005", + "@oxlint-tsgolint/linux-arm64": "7.0.2005", + "@oxlint-tsgolint/linux-x64": "7.0.2005", + "@oxlint-tsgolint/win32-arm64": "7.0.2005", + "@oxlint-tsgolint/win32-x64": "7.0.2005" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2097,9 +2024,9 @@ } }, "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-3.0.0.tgz", + "integrity": "sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2120,29 +2047,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2150,36 +2054,6 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2422,85 +2296,39 @@ "node": "20 || >=22" } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", - "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.72.0", - "@typescript-eslint/parser": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/utils": "8.72.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/undici-types": { @@ -2510,16 +2338,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2629,16 +2447,6 @@ "node": ">= 8" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workerpool": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", diff --git a/packages/lsp-profiler-ui/package.json b/packages/lsp-profiler-ui/package.json index 6a79bf9..a7bb9db 100644 --- a/packages/lsp-profiler-ui/package.json +++ b/packages/lsp-profiler-ui/package.json @@ -1,7 +1,7 @@ { "name": "@nimblesite/lsp-profiler-ui", - "version": "0.0.1", - "description": "Editor-neutral VS Code UI for the lsp-profiler protocol: processes view, CPU heat map, flame graph, memory dashboard, retention graph, autopilot.", + "version": "0.0.0-dev", + "description": "Editor-neutral VS Code UI for the lsp-profiler protocol (processes view, CPU heat map, flame graph, memory dashboard, retention graph, autopilot) plus language-neutral Debug Adapter Protocol glue and test harness (debug, debug/testing).", "license": "MIT", "repository": "https://github.com/Nimblesite/lspkit", "main": "./out/src/index.js", @@ -9,6 +9,7 @@ "files": [ "out/src", "!out/src/test", + "!out/src/debug/test", "scripts/contributions.mjs" ], "engines": { @@ -18,10 +19,11 @@ "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "build": "npm run compile", - "lint": "tsc -p ./ --noEmit && eslint .", + "lint": "tsc -p ./ --noEmit && oxlint -c ../.oxlintrc.json --type-aware --deny-warnings src", "pretest": "node scripts/write-test-host-manifest.mjs", - "test": "vscode-test", - "test:paths": "mocha --ui tdd out/src/test/suite/editor-path-key.test.js" + "test": "vscode-test && npm run test:debug", + "test:paths": "mocha --ui tdd out/src/test/suite/editor-path-key.test.js", + "test:debug": "c8 mocha --ui tdd --timeout 10000 \"out/src/debug/test/**/*.test.js\"" }, "dependencies": { "@preact/signals-core": "^1.14.4" @@ -30,25 +32,24 @@ "vscode-languageclient": "^9.0.1 || ^10.1.0" }, "overrides": { - "mocha": "$mocha" + "mocha": "$mocha", + "glob": "$glob", + "minimatch": "^11.0.3" }, "devDependencies": { - "@eslint/js": "^10.0.1", "@types/mocha": "^10.0.10", "@types/node": "^26.1.2", "@types/vscode": "^1.99.0", "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^3.1.0", - "eslint": "^10.13.0", - "glob": "^13.0.6", + "c8": "^12.0.0", + "glob": "^14.0.3", "mocha": "^12.0.3", - "typescript": "~6.0.3", - "typescript-eslint": "^8.72.0", + "oxlint": "^1.90.0", + "oxlint-tsgolint": "^7.0.2005", + "typescript": "^7.0.2", "vscode-languageclient": "^10.1.0", "vscode-languageserver": "^10.0.0", "vscode-languageserver-textdocument": "^1.0.15" - }, - "overrides": { - "mocha": "$mocha" } } diff --git a/packages/lsp-profiler-ui/src/bind-host.ts b/packages/lsp-profiler-ui/src/bind-host.ts new file mode 100644 index 0000000..ae2c201 --- /dev/null +++ b/packages/lsp-profiler-ui/src/bind-host.ts @@ -0,0 +1,19 @@ +// Implements [PROF-UI-STATE]. Host binding for the profiler UI. +import * as vscode from "vscode"; +import { DEFAULT_PALETTE, configurePalette, resolvePalette } from "./palette"; +import type { ProfilerHost } from "./host"; +import { installIds } from "./ids"; +import { bindLogger } from "./logger"; + +/** The host palette under the user's `profiler.palette` overrides, read on every render. */ +function bindHostPalette(host: ProfilerHost): void { + configurePalette(() => resolvePalette(host.palette ?? DEFAULT_PALETTE, + vscode.workspace.getConfiguration(host.configSection).get("profiler.palette"))); +} + +/** Install `host`'s ids, palette and logger for every module that reads them. */ +export function bindHost(host: ProfilerHost): void { + installIds(host); + bindHostPalette(host); + bindLogger(() => host.logger); +} diff --git a/packages/lsp-profiler-ui/src/dap-evaluate.ts b/packages/lsp-profiler-ui/src/dap-evaluate.ts index 25ef114..fb0419f 100644 --- a/packages/lsp-profiler-ui/src/dap-evaluate.ts +++ b/packages/lsp-profiler-ui/src/dap-evaluate.ts @@ -17,7 +17,7 @@ import { numberField, recordArrayField, stringField } from "./unknown-shape"; import { debugType } from "./ids"; import * as vscode from "vscode"; -import * as fs from "fs"; +import * as fs from "node:fs"; import { Logger } from "./logger"; import { errorText } from "./failure"; import { ALL_THREADS, debugOutputCursor, debugOutputSince, stoppedThreadIds } from "./dap-output"; @@ -93,17 +93,24 @@ export async function evaluateInDebugSession( if (session === undefined) { return null; } const cursor = debugOutputCursor(session.id); try { - const request = { expression, context, ...(frameId === undefined ? {} : { frameId }) }; - const direct = stringField(await session.customRequest("evaluate", request), "result") ?? ""; - if (direct.includes(MARKER_PREFIX)) { return await resolveMarkerFilePayload(direct); } - const printed = await waitForMarkerOutput(session.id, cursor); - return await resolveMarkerFilePayload(printed.length > 0 ? printed : direct); - } catch (err: unknown) { - Logger.warn(`[Memory] evaluate failed: ${errorText(err)}`); + const direct = stringField( + await session.customRequest("evaluate", { expression, context, ...(frameId === undefined ? {} : { frameId }) }), + "result", + ) ?? ""; + return await resolveMarkerFilePayload(await recoverPrinted(session, cursor, direct)); + } catch (error: unknown) { + Logger.warn(`[Memory] evaluate failed: ${errorText(error)}`); return null; } } +/** The direct evaluate result, or the printed marker output recovered after it. */ +async function recoverPrinted(session: vscode.DebugSession, cursor: number, direct: string): Promise { + if (direct.includes(MARKER_PREFIX)) { return direct; } + const printed = await waitForMarkerOutput(session.id, cursor); + return printed.length > 0 ? printed : direct; +} + /** * Resolve a file-handoff payload. Large memory snapshots are written to a temp * file by the injection script — which prints only `__LSPPROF_MEM_FILE__` @@ -128,25 +135,48 @@ export async function resolveMarkerFilePayload(out: string): Promise { /** The path after `__LSPPROF_MEM_FILE__`, or "" when the output hands off no file. */ function payloadFilePath(out: string): string { const at = out.indexOf(FILE_PAYLOAD_MARKER); - return at === -1 ? "" : out.slice(at + FILE_PAYLOAD_MARKER.length).split(/\r?\n/, 1)[0]?.trim() ?? ""; + return at === -1 ? "" : out.slice(at + FILE_PAYLOAD_MARKER.length).split(/\r?\n/u, 1)[0]?.trim() ?? ""; } /** The payload once the worker filled the file, which is then deleted; undefined when it is missing or stays empty. */ async function readPayloadFile(path: string): Promise { - const deadline = Date.now() + FILE_PAYLOAD_WAIT_MS; - for (;;) { - const contents = await fs.promises.readFile(path, "utf8").catch((err: unknown) => { - Logger.warn(`[Memory] could not read payload file: ${errorText(err)}`); - return undefined; - }); - if (contents === undefined) { return undefined; } - const expired = Date.now() >= deadline; - if (contents.length > 0 || expired) { - if (contents.length === 0) { Logger.warn(`[Memory] payload file stayed empty for ${String(FILE_PAYLOAD_WAIT_MS)}ms: ${path}`); } - await fs.promises.unlink(path).catch(() => undefined); - return contents.length > 0 ? contents : undefined; - } - await delay(FILE_PAYLOAD_POLL_MS); + return readPayloadUntil(path, Date.now() + FILE_PAYLOAD_WAIT_MS); +} + +/** Poll for the payload file until it holds data or `deadline` (epoch) passes. */ +async function readPayloadUntil(path: string, deadline: number): Promise { + const contents = await readPayloadContents(path); + if (contents === undefined) { return undefined; } + if (contents.length > 0 || Date.now() >= deadline) { + return finalizePayloadFile(path, contents); + } + await delay(FILE_PAYLOAD_POLL_MS); + return readPayloadUntil(path, deadline); +} + +/** Read the payload file, warning and returning undefined when it cannot be read. */ +async function readPayloadContents(path: string): Promise { + try { + return await fs.promises.readFile(path, "utf8"); + } catch (error: unknown) { + Logger.warn(`[Memory] could not read payload file: ${errorText(error)}`); + return undefined; + } +} + +/** Delete a settled payload file and return its non-empty contents, if any. */ +async function finalizePayloadFile(path: string, contents: string): Promise { + if (contents.length === 0) { Logger.warn(`[Memory] payload file stayed empty for ${String(FILE_PAYLOAD_WAIT_MS)}ms: ${path}`); } + await removePayloadFile(path); + return contents.length > 0 ? contents : undefined; +} + +/** Best-effort delete of a payload file; a missing file is already the outcome. */ +async function removePayloadFile(path: string): Promise { + try { + await fs.promises.unlink(path); + } catch { + // The worker may have replaced or removed it; nothing to clean up. } } @@ -160,19 +190,22 @@ async function readPayloadFile(path: string): Promise { * the first `\n` after the marker reliably ends the payload. */ async function waitForMarkerOutput(sessionId: string, cursor: number): Promise { - const deadline = Date.now() + MARKER_WAIT_MS; - for (;;) { - const out = debugOutputSince(sessionId, cursor); - const markerAt = out.indexOf(MARKER_PREFIX); - // The payload line is complete once a newline follows the marker (the - // `print()` terminator); `includes(.., markerAt)` searches from the marker. - if (markerAt !== -1 && out.includes("\n", markerAt)) { return out; } - if (Date.now() >= deadline) { - warnMarkerWaitExpired(out, markerAt); - return out; - } - await delay(MARKER_POLL_MS); + return waitForMarkerUntil(sessionId, cursor, Date.now() + MARKER_WAIT_MS); +} + +/** Poll for marker output until its line is newline-terminated or `deadline` passes. */ +async function waitForMarkerUntil(sessionId: string, cursor: number, deadline: number): Promise { + const out = debugOutputSince(sessionId, cursor); + const markerAt = out.indexOf(MARKER_PREFIX); + // The payload line is complete once a newline follows the marker (the + // `print()` terminator); `includes(.., markerAt)` searches from the marker. + if (markerAt !== -1 && out.includes("\n", markerAt)) { return out; } + if (Date.now() >= deadline) { + warnMarkerWaitExpired(out, markerAt); + return out; } + await delay(MARKER_POLL_MS); + return waitForMarkerUntil(sessionId, cursor, deadline); } /** @@ -206,22 +239,31 @@ export async function currentStoppedFrameId(): Promise { if (session === undefined) { return null; } const stopped = stoppedThreadIds(session.id); if (stopped.length === 0) { return null; } + return stoppedFrameIdFor(session, stopped); +} + +/** The top evaluable frame among `stopped` threads, else null. */ +async function stoppedFrameIdFor(session: vscode.DebugSession, stopped: readonly number[]): Promise { try { const candidates = stopped.includes(ALL_THREADS) ? await allThreadIds(session) : stopped; return await firstTopFrameId(session, candidates); - } catch (err: unknown) { - Logger.warn(`[Memory] could not resolve a stopped frame: ${errorText(err)}`); + } catch (error: unknown) { + Logger.warn(`[Memory] could not resolve a stopped frame: ${errorText(error)}`); return null; } } /** The top frame of the first of `threadIds` that is stopped, else null. */ async function firstTopFrameId(session: vscode.DebugSession, threadIds: readonly number[]): Promise { - for (const threadId of threadIds) { - const frameId = await topFrameIdIfStopped(session, threadId); - if (frameId !== null) { return frameId; } - } - return null; + return firstTopFrameAt(session, threadIds, 0); +} + +/** The top frame of `threadIds[index]` (or later) if stopped, else null. */ +async function firstTopFrameAt(session: vscode.DebugSession, threadIds: readonly number[], index: number): Promise { + const threadId = threadIds[index]; + if (threadId === undefined) { return null; } + const frameId = await topFrameIdIfStopped(session, threadId); + return frameId ?? firstTopFrameAt(session, threadIds, index + 1); } /** Every thread id the debuggee reports (for `allThreadsStopped` stops). */ @@ -264,36 +306,45 @@ export async function acquireStoppedFrame(): Promise { const existing = await currentStoppedFrameId(); // The user's own pause: nothing to release. if (existing !== null) { return { frameId: existing, release: async () => { await Promise.resolve(); } }; } - return await pauseForFrame(session); + return pauseForFrame(session); } /** Pause the running debuggee until it stops in an evaluable frame, retrying from bootstrap frames. */ async function pauseForFrame(session: vscode.DebugSession): Promise { - const deadline = Date.now() + STARTUP_TIMEOUT_MS; - for (let attempt = 1; Date.now() < deadline; attempt += 1) { - if (!(await pauseDebuggee(session))) { return null; } - const frameId = await waitForFrameUntil(Math.min(deadline, Date.now() + WAIT_MS)); - if (frameId !== null) { - Logger.info(`[Memory] transparently paused the debuggee for evaluation (attempt ${String(attempt)})`); - return { frameId, release: async () => { await resumeDebuggee(session); } }; - } - // Paused, but no evaluable frame (bootstrap / hidden frames): resume so - // the program can reach user code, then try again. - Logger.info(`[Memory] pause landed in non-user frames (attempt ${String(attempt)}) — resuming to retry`); - await resumeDebuggee(session); - await delay(PAUSE_RETRY_BACKOFF_MS); + return pauseForFrameUntil(session, Date.now() + STARTUP_TIMEOUT_MS, 1); +} + +/** One transparent-pause attempt; recurses with the next attempt until `deadline` passes. */ +async function pauseForFrameUntil(session: vscode.DebugSession, deadline: number, attempt: number): Promise { + if (Date.now() >= deadline) { return null; } + if (!(await pauseDebuggee(session))) { return null; } + const acquired = await acquiredFrameOrResume(session, deadline, attempt); + if (acquired !== undefined) { return acquired; } + await delay(PAUSE_RETRY_BACKOFF_MS); + return pauseForFrameUntil(session, deadline, attempt + 1); +} + +/** An evaluable frame, or a resume-and-retry when the pause landed in bootstrap frames. */ +async function acquiredFrameOrResume(session: vscode.DebugSession, deadline: number, attempt: number): Promise { + const frameId = await waitForFrameUntil(Math.min(deadline, Date.now() + WAIT_MS)); + if (frameId !== null) { + Logger.info(`[Memory] transparently paused the debuggee for evaluation (attempt ${String(attempt)})`); + return { frameId, release: async () => { await resumeDebuggee(session); } }; } - return null; + // Paused, but no evaluable frame (bootstrap / hidden frames): resume so + // the program can reach user code, then try again. + Logger.info(`[Memory] pause landed in non-user frames (attempt ${String(attempt)}) — resuming to retry`); + await resumeDebuggee(session); + return undefined; } /** Poll for an evaluable stopped frame until `deadlineMs` (epoch), else null. */ async function waitForFrameUntil(deadlineMs: number): Promise { - while (Date.now() < deadlineMs) { - const frameId = await currentStoppedFrameId(); - if (frameId !== null) { return frameId; } - await delay(POLL_INTERVAL_MS); - } - return null; + if (Date.now() >= deadlineMs) { return null; } + const frameId = await currentStoppedFrameId(); + if (frameId !== null) { return frameId; } + await delay(POLL_INTERVAL_MS); + return waitForFrameUntil(deadlineMs); } /** Ask debugpy to pause the first reported thread. */ @@ -303,8 +354,8 @@ async function pauseDebuggee(session: vscode.DebugSession): Promise { if (ids.length === 0) { return false; } await session.customRequest("pause", { threadId: ids[0] }); return true; - } catch (err: unknown) { - Logger.warn(`[Memory] pause failed: ${errorText(err)}`); + } catch (error: unknown) { + Logger.warn(`[Memory] pause failed: ${errorText(error)}`); return false; } } @@ -312,23 +363,31 @@ async function pauseDebuggee(session: vscode.DebugSession): Promise { /** Resume the first stopped (or reported) thread after a transparent pause. */ async function resumeDebuggee(session: vscode.DebugSession): Promise { try { - const stopped = stoppedThreadIds(session.id).filter((id) => id !== ALL_THREADS); - const threadId = stopped.at(0) ?? (await allThreadIds(session)).at(0); + const threadId = await resumeThreadId(session); if (threadId === undefined) { return; } await session.customRequest("continue", { threadId }); Logger.info("[Memory] resumed the debuggee after evaluation"); - } catch (err: unknown) { - Logger.warn(`[Memory] resume failed: ${errorText(err)}`); + } catch (error: unknown) { + Logger.warn(`[Memory] resume failed: ${errorText(error)}`); } } +/** The thread to resume: the first stopped one, else the first reported one. */ +async function resumeThreadId(session: vscode.DebugSession): Promise { + const stopped = stoppedThreadIds(session.id).filter((id) => id !== ALL_THREADS); + const firstStopped = stopped.at(0); + if (firstStopped !== undefined) { return firstStopped; } + const allIds = await allThreadIds(session); + return allIds.at(0); +} + /** * Poll for a stopped frame until the startup budget runs out. Shared by the * memory track-on-launch flow and the cooperative CPU sampler injection, * both of which wait for the `stopOnEntry` pause before evaluating. */ export async function waitForStoppedFrame(): Promise { - return await waitForFrameUntil(Date.now() + STARTUP_TIMEOUT_MS); + return waitForFrameUntil(Date.now() + STARTUP_TIMEOUT_MS); } /** Top frameId of `threadId` if it is stopped, else null (running threads error). */ @@ -344,6 +403,7 @@ async function topFrameIdIfStopped( }); return numberField(recordArrayField(stack, "stackFrames")[0], "id") ?? null; } catch { - return null; // thread not suspended + // Thread not suspended. + return null; } } diff --git a/packages/lsp-profiler-ui/src/dap-output.ts b/packages/lsp-profiler-ui/src/dap-output.ts index c8efee1..48e6e9e 100644 --- a/packages/lsp-profiler-ui/src/dap-output.ts +++ b/packages/lsp-profiler-ui/src/dap-output.ts @@ -27,10 +27,10 @@ import { booleanField, numberField, rawField, stringField } from "./unknown-shap const MAX_BUFFER_CHARS = 1_000_000; -/** sessionId → accumulated output text. */ +/** Maps sessionId → accumulated output text. */ const buffers = new Map(); -/** sessionId → thread ids reported stopped (empty/absent = running). */ +/** Maps sessionId → thread ids reported stopped (empty/absent = running). */ const stoppedThreads = new Map>(); /** Sentinel for a `stopped` event carrying `allThreadsStopped` but no id. */ @@ -89,7 +89,7 @@ const RESUME_COMMANDS = new Set([ * says otherwise (`allThreadsContinued` defaults to true per the spec). */ const ALL_THREAD_RESUMES = new Set(["continue", "reverseContinue"]); -/** sessionId → seq of an in-flight resume request → its command + threadId. */ +/** Maps sessionId → seq of an in-flight resume request → its command + threadId. */ const pendingResumes = new Map>(); /** Remember an outgoing resume-implying request (DAP tracker, editor → adapter). */ @@ -111,23 +111,37 @@ export function trackResumeRequest(sessionId: string, message: unknown): void { * re-arms the bookkeeping when the step lands. */ export function trackResumeResponse(sessionId: string, message: unknown): void { - const requestSeq = numberField(message, "request_seq"); - if (stringField(message, "type") !== "response" || requestSeq === undefined) { return; } - const pending = pendingResumes.get(sessionId); - const request = pending?.get(requestSeq); - if (pending === undefined || request === undefined) { return; } - pending.delete(requestSeq); - if (pending.size === 0) { pendingResumes.delete(sessionId); } + const pending = pendingResumeFor(sessionId, message); + if (pending === undefined) { return; } + clearPendingResume(sessionId, pending.requestSeq); if (booleanField(message, "success") !== true) { return; } - const allThreads = - ALL_THREAD_RESUMES.has(request.command) && - booleanField(rawField(message, "body"), "allThreadsContinued") !== false; trackSuspensionEvent(sessionId, "continued", { - threadId: request.threadId, - allThreadsContinued: allThreads, + threadId: pending.request.threadId, + allThreadsContinued: resumeCoversAllThreads(message, pending.request), }); } +/** The in-flight resume request `message` answers, when it is a successful-looking response. */ +function pendingResumeFor(sessionId: string, message: unknown): { requestSeq: number; request: { command: string; threadId?: number } } | undefined { + const requestSeq = numberField(message, "request_seq"); + if (stringField(message, "type") !== "response" || requestSeq === undefined) { return undefined; } + const request = pendingResumes.get(sessionId)?.get(requestSeq); + return request === undefined ? undefined : { requestSeq, request }; +} + +/** Drop one in-flight resume request and, when it was the last, the session's map. */ +function clearPendingResume(sessionId: string, requestSeq: number): void { + const pending = pendingResumes.get(sessionId); + pending?.delete(requestSeq); + if (pending !== undefined && pending.size === 0) { pendingResumes.delete(sessionId); } +} + +/** Whether a successful resume covers every thread (`allThreadsContinued` defaults true). */ +function resumeCoversAllThreads(message: unknown, request: { command: string }): boolean { + return ALL_THREAD_RESUMES.has(request.command) + && booleanField(rawField(message, "body"), "allThreadsContinued") !== false; +} + /** Append a chunk of debuggee output for a session (called by the DAP tracker). */ export function appendDebugOutput(sessionId: string, text: string): void { const combined = (buffers.get(sessionId) ?? "") + text; diff --git a/packages/lsp-profiler-ui/src/debug-link-setup.ts b/packages/lsp-profiler-ui/src/debug-link-setup.ts new file mode 100644 index 0000000..aad6404 --- /dev/null +++ b/packages/lsp-profiler-ui/src/debug-link-setup.ts @@ -0,0 +1,18 @@ +// Implements [PROF-UI]. See docs/specs/LSP-PROFILER-SPEC.md +import type * as vscode from "vscode"; +import { registerDebugProfilerLink } from "./debug-link"; +import { walkReferences } from "./memory-ref-picker"; +import type { ProfilerStore } from "./store"; + +/** A debugger variable's type retainers, from the memory session tracking that same debug session. */ +function typeRetainers(store: ProfilerStore): (type: string, sessionId: string) => Promise { + return async (type, sessionId) => { + if (store.profiler.value.memoryDebugSessionId !== sessionId) { throw new Error("Track this debug session before inspecting type retainers."); } + await walkReferences(store, type); + }; +} + +/** Register the debugger "inspect type" link against the profiler store. */ +export function registerDebugProfilerSetup(store: ProfilerStore, debugType: string): vscode.Disposable[] { + return registerDebugProfilerLink({ debugType, inspectType: typeRetainers(store) }); +} diff --git a/packages/lsp-profiler-ui/src/debug-link-view.ts b/packages/lsp-profiler-ui/src/debug-link-view.ts index c0112f2..640e4da 100644 --- a/packages/lsp-profiler-ui/src/debug-link-view.ts +++ b/packages/lsp-profiler-ui/src/debug-link-view.ts @@ -1,5 +1,6 @@ // Implements [PROF-UI-DEBUG-LINK]. Shared watch context in CPU, memory and retention panels. import { selectedProfileVariable } from "./debug-selection"; + export const DEBUG_LINK_BODY = `