diff --git a/.github/scripts/validate-mcp-registry.mjs b/.github/scripts/validate-mcp-registry.mjs index 0633a91b..210504d1 100644 --- a/.github/scripts/validate-mcp-registry.mjs +++ b/.github/scripts/validate-mcp-registry.mjs @@ -205,15 +205,12 @@ function main() { const packageArguments = npmPackage.packageArguments ?? [] assert.deepEqual( packageArguments.map((entry) => entry.value ?? entry.valueHint ?? null), - ['serve', '--stdio', 'graph_path'], - 'server.json package arguments must model `madar serve --stdio `', + ['serve', '--stdio', '--auto-refresh'], + 'server.json package arguments must model `madar serve --stdio --auto-refresh`', ) const graphPathArgument = packageArguments.find((entry) => entry.valueHint === 'graph_path') - assert.ok(graphPathArgument, 'server.json must require a graph_path positional argument') - assert.equal(graphPathArgument.default, 'out/graph.json', 'graph_path should default to out/graph.json') - assert.equal(graphPathArgument.format, 'filepath', 'graph_path should be marked as a filepath input') - assert.equal(graphPathArgument.isRequired, true, 'graph_path should be required') + assert.equal(graphPathArgument, undefined, 'server.json must not pin the MCP server to a static graph_path argument') const toolProfile = (npmPackage.environmentVariables ?? []).find((entry) => entry.name === 'MADAR_TOOL_PROFILE') assert.ok(toolProfile, 'server.json must describe the MADAR_TOOL_PROFILE environment variable') diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e37842..3dac6496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to the TypeScript package will be documented in this file. +## [0.30.0] - 2026-07-14 + +### Added + +- **Installed MCP integrations now keep their graph current automatically**: newly generated Claude Code, Codex, Cursor, Copilot, Gemini, Aider, and OpenCode configurations launch `madar serve --stdio --auto-refresh`. The server reconciles the active workspace at startup, watches it while the agent session is active, and publishes refreshed graph artifacts atomically after source or relevant configuration changes. Re-run your agent's `madar install` command after upgrading to update an existing managed MCP entry. Closes #545. +- **Linked Git worktrees now receive isolated Madar artifacts**: default graphs, caches, reports, compare output, and time-travel artifacts live outside a linked checkout in its repository's shared Git data directory, with a distinct artifact directory for each worktree. This prevents branches from sharing or overwriting graph state while keeping generated artifacts out of the checkout. Closes #546. + +### Notes + +- **An MCP server is scoped to the worktree it started in**: start or reconnect the agent/MCP server from the intended worktree. A running server cannot follow an agent that later changes directory or creates and moves into another worktree. + ## [0.29.0] - 2026-07-12 ### Added diff --git a/README.md b/README.md index 5d6f03e2..bcd59230 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ madar opencode install After installing a profile, run `madar doctor` and `madar status`. Installer details are in the [CLI and MCP reference](https://github.com/mohanagy/madar/blob/main/docs/reference/cli-and-mcp.md). +If you upgrade to `0.30.0` from an earlier version, run your profile's install command again (for example, `madar claude install` or `madar codex install`) to update its managed MCP entry with automatic refresh. + ## Use Without MCP You can also generate context directly from the CLI: @@ -138,9 +140,9 @@ It helps less when: - the task is obvious from one file - the question needs live runtime behavior - the code relies heavily on dynamic patterns static analysis cannot see -- the generated graph is stale after large repo changes +- you use a standalone graph without regenerating it after large repo changes -If the repo changed a lot, regenerate: +For standalone CLI workflows, regenerate after substantial repo changes: ```bash madar generate . @@ -150,6 +152,8 @@ madar generate . Madar records graph freshness so agents can tell whether context still matches the repo. On git workspaces, freshness is tied to the graph build commit plus the working-tree diff, so unrelated changes do not have to block a focused task by default. +Installed MCP profiles in `0.30.0` start `madar serve --stdio --auto-refresh`. Madar reconciles the graph when that server starts, then watches the active workspace and refreshes the graph after source or relevant configuration changes. You do not need to run `madar generate` after every agent edit or session; manual generation remains available for standalone CLI workflows. + ```bash madar pack "how does auth work?" --require-fresh-context madar pack "how does auth work?" --require-fresh-graph @@ -157,6 +161,12 @@ madar pack "how does auth work?" --require-fresh-graph Use `--require-fresh-context` when the selected files must be fresh. Use `--require-fresh-graph` when the whole graph must match the current repo. +## Git Worktrees + +Run Madar and your coding agent from the same linked Git worktree. Madar keeps the default graph and related artifacts outside that checkout, under the repository's shared Git data directory, and gives each worktree its own isolated artifact directory. That keeps branches from sharing graph state and avoids generated `out/` artifacts inside linked worktrees. + +An MCP server selects its workspace when it starts. If an agent later creates or switches to another worktree, start or reconnect the agent/MCP server from that new worktree; a running server cannot follow a later directory change. + ## Evidence Madar now has proof-backed public TypeScript `explain-runtime` legacy benchmark receipts across six open-source repos. Each row below has `benchmark_outcome = "full_win"`, `benchmark_readiness = "ready"`, `answer_quality.madar.passed = true`, and `answer_contract.runtime_proof.missing_obligations = []`. @@ -200,13 +210,15 @@ It does not record prompt text, answer text, source paths, source content, or re ## What's New -Current version: `0.29.0`. +Current version: `0.30.0`. + +`0.30.0` makes installed MCP integrations self-refreshing: they reconcile the graph at startup and watch the active workspace through an agent session. It also gives each linked Git worktree isolated external graph and artifact storage. Start or reconnect MCP from the worktree the agent is using; a running server stays scoped to the worktree where it started. `0.29.0` adds full project-local Codex CLI wiring: `madar codex install` now owns a task-applicable `UserPromptSubmit` hook, its local script, and a marker-owned Madar MCP entry alongside the AGENTS profile. The hook provides guidance for local code tasks, not enforcement; review and trust it in Codex before relying on it. `0.28.0` promoted the public benchmark work to a proof-backed stable release: six public TypeScript `explain-runtime` legacy rows now have checked-in `full_win` receipts, strict runtime-proof gates, direct-evidence answer checks, scoped benchmark roots, and share-safe reports. It also includes retrieval and extraction improvements for runtime handoffs, source-visible framework flows, and benchmark reproducibility. -Read the full notes in the [0.29.0 changelog](https://github.com/mohanagy/madar/blob/main/CHANGELOG.md#0290---2026-07-12). +Read the full notes in the [0.30.0 changelog](https://github.com/mohanagy/madar/blob/main/CHANGELOG.md#0300---2026-07-14). ## Docs diff --git a/docs/mcp-registry/server.json b/docs/mcp-registry/server.json index 2145ca0c..4f0405b7 100644 --- a/docs/mcp-registry/server.json +++ b/docs/mcp-registry/server.json @@ -9,13 +9,13 @@ "source": "github", "url": "https://github.com/mohanagy/madar" }, - "version": "0.29.0", + "version": "0.30.0", "packages": [ { "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@lubab/madar", - "version": "0.29.0", + "version": "0.30.0", "runtimeHint": "npx", "transport": { "type": "stdio" @@ -31,11 +31,7 @@ }, { "type": "positional", - "valueHint": "graph_path", - "description": "Path to out/graph.json. Run `madar generate .` first so the local MCP server has a graph artifact to serve.", - "default": "out/graph.json", - "format": "filepath", - "isRequired": true + "value": "--auto-refresh" } ], "environmentVariables": [ @@ -53,7 +49,7 @@ ], "_meta": { "io.modelcontextprotocol.registry/publisher-provided": { - "notes": "Public npm package plus local graph artifact install flow. Madar is the renamed continuation of `graphify-ts`; use `@lubab/madar` and `https://github.com/mohanagy/madar` as the canonical package and repository.", + "notes": "Public npm package plus local auto-refresh MCP flow. The registry starts `madar serve --stdio --auto-refresh` in the active workspace, so Madar builds and refreshes its local graph without a static graph-path argument. Madar is the renamed continuation of `graphify-ts`; use `@lubab/madar` and `https://github.com/mohanagy/madar` as the canonical package and repository.", "source": "docs/mcp-registry/server.json" } } diff --git a/docs/reference/cli-and-mcp.md b/docs/reference/cli-and-mcp.md index 19e4ab71..73f0aad6 100644 --- a/docs/reference/cli-and-mcp.md +++ b/docs/reference/cli-and-mcp.md @@ -43,7 +43,7 @@ The checked-in public registry manifest lives at [`docs/mcp-registry/server.json npm run registry:validate ``` -The official MCP Registry hosts metadata, not Madar code or your local graph artifact. Madar's registry entry points back to the public npm package and the same local-first runtime flow: run `madar generate .` to create `out/graph.json`, then start the local stdio server with `npx @lubab/madar serve --stdio out/graph.json`, or let `madar install` write that wiring for you. Generated Claude and Cursor MCP configs now call the installed `madar` command as `madar serve --stdio ` instead of writing a version-pinned `npx` launcher into the repo-local config. +The official MCP Registry hosts metadata, not Madar code or your local graph artifact. Its entry starts `npx @lubab/madar serve --stdio --auto-refresh` from the active workspace: Madar creates the graph when needed, then refreshes it after local changes. Do not add a fixed `out/graph.json` argument to that registry command, because it would become stale and would not follow a linked Git worktree's isolated artifact directory. Start or reconnect the MCP server from each worktree the agent enters. Generated agent MCP configs use the installed `madar` command with the same `serve --stdio --auto-refresh` flow rather than a version-pinned `npx` launcher or an absolute graph path. If you still discover older `graphify-ts` links or listings, Madar is the current project name. Use `https://github.com/mohanagy/madar` and `@lubab/madar` as the canonical repository and package surfaces. diff --git a/package-lock.json b/package-lock.json index 0ae42b8a..f0f28959 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lubab/madar", - "version": "0.29.0", + "version": "0.30.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lubab/madar", - "version": "0.29.0", + "version": "0.30.0", "license": "MIT", "dependencies": { "@vscode/tree-sitter-wasm": "^0.3.1", diff --git a/package.json b/package.json index fbc88352..0879dd6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lubab/madar", - "version": "0.29.0", + "version": "0.30.0", "description": "Stop AI coding agents from rediscovering large TypeScript/Node repos. Madar compiles task-aware local context packs from what runs for this task.", "license": "MIT", "author": "mohanagy", diff --git a/sbom.cdx.json b/sbom.cdx.json index 4b652d9a..2c388428 100644 --- a/sbom.cdx.json +++ b/sbom.cdx.json @@ -2,13 +2,13 @@ "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.5", - "serialNumber": "urn:uuid:a4821d33-24d3-406a-a718-004bf84b8112", + "serialNumber": "urn:uuid:8e4e235e-1a84-4a31-baa4-32bf09a7b99e", "version": 1, "metadata": { - "timestamp": "2026-07-12T18:16:44.257Z", + "timestamp": "2026-07-14T06:44:20.707Z", "lifecycles": [ { - "phase": "pre-build" + "phase": "build" } ], "tools": [ @@ -19,14 +19,14 @@ } ], "component": { - "bom-ref": "@lubab/madar@0.29.0", + "bom-ref": "@lubab/madar@0.30.0", "type": "library", "name": "madar", - "version": "0.29.0", + "version": "0.30.0", "scope": "required", "author": "mohanagy", "description": "Stop AI coding agents from rediscovering large TypeScript/Node repos. Madar compiles task-aware local context packs from what runs for this task.", - "purl": "pkg:npm/%40lubab/madar@0.29.0", + "purl": "pkg:npm/%40lubab/madar@0.30.0", "properties": [], "externalReferences": [ { @@ -58,6 +58,8 @@ "name": "@babel/helper-string-parser", "version": "7.27.1", "scope": "required", + "author": "The Babel Team (https://babel.dev/team)", + "description": "A utility package to parse strings", "purl": "pkg:npm/%40babel/helper-string-parser@7.27.1", "properties": [ { @@ -65,7 +67,16 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/babel/babel.git" + }, + { + "type": "website", + "url": "https://babel.dev/docs/en/next/babel-helper-string-parser" + } + ], "licenses": [ { "license": { @@ -80,6 +91,8 @@ "name": "@babel/helper-validator-identifier", "version": "7.28.5", "scope": "required", + "author": "The Babel Team (https://babel.dev/team)", + "description": "Validate identifier/keywords name", "purl": "pkg:npm/%40babel/helper-validator-identifier@7.28.5", "properties": [ { @@ -87,7 +100,12 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/babel/babel.git" + } + ], "licenses": [ { "license": { @@ -102,6 +120,8 @@ "name": "@babel/parser", "version": "7.29.2", "scope": "required", + "author": "The Babel Team (https://babel.dev/team)", + "description": "A JavaScript parser", "purl": "pkg:npm/%40babel/parser@7.29.2", "properties": [ { @@ -109,82 +129,14 @@ "value": "true" } ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "@babel/types@7.29.0", - "type": "library", - "name": "@babel/types", - "version": "7.29.0", - "scope": "required", - "purl": "pkg:npm/%40babel/types@7.29.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "@bcoe/v8-coverage@1.0.2", - "type": "library", - "name": "@bcoe/v8-coverage", - "version": "1.0.2", - "scope": "required", - "purl": "pkg:npm/%40bcoe/v8-coverage@1.0.2", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "@emnapi/core@1.11.1", - "type": "library", - "name": "@emnapi/core", - "version": "1.11.1", - "scope": "optional", - "purl": "pkg:npm/%40emnapi/core@1.11.1", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "https://github.com/babel/babel.git" + }, { - "alg": "SHA-512", - "content": "452bdb4261f374accdb0b61aff01eb6dcdca378b182ca01d3d9c6a88cd87013aaffd2064dbf10d487a6f5c668b38c72c032cf4a68106aa49a62981b739688911" + "type": "website", + "url": "https://babel.dev/docs/en/next/babel-parser" } ], "licenses": [ @@ -196,12 +148,14 @@ ] }, { - "bom-ref": "@emnapi/runtime@1.11.1", + "bom-ref": "@babel/types@7.29.0", "type": "library", - "name": "@emnapi/runtime", - "version": "1.11.1", - "scope": "optional", - "purl": "pkg:npm/%40emnapi/runtime@1.11.1", + "name": "@babel/types", + "version": "7.29.0", + "scope": "required", + "author": "The Babel Team (https://babel.dev/team)", + "description": "Babel Types is a Lodash-esque utility library for AST nodes", + "purl": "pkg:npm/%40babel/types@7.29.0", "properties": [ { "name": "cdx:npm:package:development", @@ -210,14 +164,12 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "https://github.com/babel/babel.git" + }, { - "alg": "SHA-512", - "content": "be08fb477cb75a0c76e0841a18f03f47a6055cb1d530e674b951322103da5acfab7750337c43179400b6d856303b55e4291e8d3ecabb9946a7747f2821175917" + "type": "website", + "url": "https://babel.dev/docs/en/next/babel-types" } ], "licenses": [ @@ -229,12 +181,14 @@ ] }, { - "bom-ref": "@emnapi/wasi-threads@1.2.2", + "bom-ref": "@bcoe/v8-coverage@1.0.2", "type": "library", - "name": "@emnapi/wasi-threads", - "version": "1.2.2", - "scope": "optional", - "purl": "pkg:npm/%40emnapi/wasi-threads@1.2.2", + "name": "@bcoe/v8-coverage", + "version": "1.0.2", + "scope": "required", + "author": "Charles Samborski (https://demurgos.net)", + "description": "Helper functions for V8 coverage files.", + "purl": "pkg:npm/%40bcoe/v8-coverage@1.0.2", "properties": [ { "name": "cdx:npm:package:development", @@ -243,14 +197,8 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "73de6a39790777274d2a1b1c05379ba840b5095019a72a8e7d57c1cd0d6a833ca5de07de95d5232208036c86600cab072e09ec33e8a01fb4c9fde01ab1a56e30" + "type": "vcs", + "url": "git://github.com/bcoe/v8-coverage.git" } ], "licenses": [ @@ -267,6 +215,8 @@ "name": "@jridgewell/resolve-uri", "version": "3.1.2", "scope": "required", + "author": "Justin Ridgewell ", + "description": "Resolve a URI relative to an optional base URI", "purl": "pkg:npm/%40jridgewell/resolve-uri@3.1.2", "properties": [ { @@ -289,6 +239,8 @@ "name": "@jridgewell/sourcemap-codec", "version": "1.5.5", "scope": "required", + "author": "Justin Ridgewell ", + "description": "Encode/decode sourcemap mappings", "purl": "pkg:npm/%40jridgewell/sourcemap-codec@1.5.5", "properties": [ { @@ -296,7 +248,16 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git+https://github.com/jridgewell/sourcemaps.git" + }, + { + "type": "website", + "url": "https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec" + } + ], "licenses": [ { "license": { @@ -311,6 +272,8 @@ "name": "@jridgewell/trace-mapping", "version": "0.3.31", "scope": "required", + "author": "Justin Ridgewell ", + "description": "Trace the original position through a source map", "purl": "pkg:npm/%40jridgewell/trace-mapping@0.3.31", "properties": [ { @@ -318,38 +281,14 @@ "value": "true" } ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "@napi-rs/wasm-runtime@1.1.6", - "type": "library", - "name": "@napi-rs/wasm-runtime", - "version": "1.1.6", - "scope": "optional", - "purl": "pkg:npm/%40napi-rs/wasm-runtime@1.1.6", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+https://github.com/jridgewell/sourcemaps.git" + }, { - "alg": "SHA-512", - "content": "64bbff25d51f92f3b2f5e0a79c16867e23be5e299b8de6c078ef8c450a83fc1f85475b6744dd2da4a4891d16c4f2c15f4ba6aab1767aed34237f07ecc542d56e" + "type": "website", + "url": "https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping" } ], "licenses": [ @@ -366,6 +305,8 @@ "name": "@oxc-project/types", "version": "0.139.0", "scope": "required", + "author": "Boshen and oxc contributors", + "description": "Types for Oxc AST nodes", "purl": "pkg:npm/%40oxc-project/types@0.139.0", "properties": [ { @@ -377,6 +318,14 @@ { "type": "distribution", "url": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/oxc-project/oxc.git" + }, + { + "type": "website", + "url": "https://oxc.rs" } ], "hashes": [ @@ -394,12 +343,13 @@ ] }, { - "bom-ref": "@rolldown/binding-android-arm64@1.1.5", + "bom-ref": "@rolldown/binding-darwin-arm64@1.1.5", "type": "library", - "name": "@rolldown/binding-android-arm64", + "name": "@rolldown/binding-darwin-arm64", "version": "1.1.5", "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-android-arm64@1.1.5", + "description": "Fast JavaScript/TypeScript bundler in Rust with Rollup-compatible API.", + "purl": "pkg:npm/%40rolldown/binding-darwin-arm64@1.1.5", "properties": [ { "name": "cdx:npm:package:development", @@ -409,13 +359,21 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz" + "url": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/rolldown/rolldown.git" + }, + { + "type": "website", + "url": "https://rolldown.rs/" } ], "hashes": [ { "alg": "SHA-512", - "content": "95983c7ea22fdafec5176dfb6f0320cc664426f18befdfece649c9fe2e859ac185e175e5cdc719e236fe4eb148c6d492c45b48a5db20acf65e324d48f4015cc9" + "content": "e75067c7da4d88c44a49436d05fc9290d27dbcc53d1e1dc8d68cc377a8323cf63369709f9e9b547046715c66059feba5d9db5c3148aa191d586a2d8ad74ba84f" } ], "licenses": [ @@ -427,12 +385,13 @@ ] }, { - "bom-ref": "@rolldown/binding-darwin-arm64@1.1.5", + "bom-ref": "@rolldown/pluginutils@1.0.1", "type": "library", - "name": "@rolldown/binding-darwin-arm64", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-darwin-arm64@1.1.5", + "name": "@rolldown/pluginutils", + "version": "1.0.1", + "scope": "required", + "description": "Plugin utilities for Rolldown", + "purl": "pkg:npm/%40rolldown/pluginutils@1.0.1", "properties": [ { "name": "cdx:npm:package:development", @@ -442,13 +401,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz" + "url": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/rolldown/plugins.git" + }, + { + "type": "website", + "url": "https://github.com/rolldown/plugins/tree/main/packages/pluginutils#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/rolldown/plugins/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "e75067c7da4d88c44a49436d05fc9290d27dbcc53d1e1dc8d68cc377a8323cf63369709f9e9b547046715c66059feba5d9db5c3148aa191d586a2d8ad74ba84f" + "content": "da3f5b1ade4987c863faf3ed8333ed97bda3d324711c0cae9a8a3a4cd7c08ec2c1d3852da52bcf6cf703701331cfb9fef42601d1cd46c501716118368e29aa1b" } ], "licenses": [ @@ -460,12 +431,14 @@ ] }, { - "bom-ref": "@rolldown/binding-darwin-x64@1.1.5", + "bom-ref": "@standard-schema/spec@1.1.0", "type": "library", - "name": "@rolldown/binding-darwin-x64", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-darwin-x64@1.1.5", + "name": "@standard-schema/spec", + "version": "1.1.0", + "scope": "required", + "author": "Colin McDonnell", + "description": "A family of specs for interoperable TypeScript", + "purl": "pkg:npm/%40standard-schema/spec@1.1.0", "properties": [ { "name": "cdx:npm:package:development", @@ -475,13 +448,21 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz" + "url": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/standard-schema/standard-schema" + }, + { + "type": "website", + "url": "https://standardschema.dev" } ], "hashes": [ { "alg": "SHA-512", - "content": "4e6fa06df0b4687bb5b4103f26f290877d92d0ae988021e4880178fd6eb15f42b446636e73de1578ae35f5d26813ae51e5a4719a8fa7a194125ab00c17ac9bea" + "content": "976685cb98c02e19e21b91e0aab0fa8d72e2feb516acabea37fa89c7aca826c80a85b95577e8aaa94e110976af9bf8cf8adc83a394c2bca327a632a73ab8b2d3" } ], "licenses": [ @@ -493,12 +474,13 @@ ] }, { - "bom-ref": "@rolldown/binding-freebsd-x64@1.1.5", + "bom-ref": "@types/chai@5.2.3", "type": "library", - "name": "@rolldown/binding-freebsd-x64", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-freebsd-x64@1.1.5", + "name": "@types/chai", + "version": "5.2.3", + "scope": "required", + "description": "TypeScript definitions for chai", + "purl": "pkg:npm/%40types/chai@5.2.3", "properties": [ { "name": "cdx:npm:package:development", @@ -508,13 +490,21 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz" + "url": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git" + }, + { + "type": "website", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/chai" } ], "hashes": [ { "alg": "SHA-512", - "content": "24ccc3282097abddd871c1b9833de1bceb35a1744a01fd176297ce4bcf1efb066b0bc22e823ea3ebcf3aeefad8664be90c3a4a9ff2a828e45386672132917a4c" + "content": "330e79f28780f5f15bbfae7fcb8987b570ecf5b3e714c6402ff8f174f154a4e1c72175fdd667201076d2e4b6a1afea7064547c03b19095e456788e9c1850b650" } ], "licenses": [ @@ -526,12 +516,13 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-arm-gnueabihf@1.1.5", + "bom-ref": "@types/deep-eql@4.0.2", "type": "library", - "name": "@rolldown/binding-linux-arm-gnueabihf", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-arm-gnueabihf@1.1.5", + "name": "@types/deep-eql", + "version": "4.0.2", + "scope": "required", + "description": "TypeScript definitions for deep-eql", + "purl": "pkg:npm/%40types/deep-eql@4.0.2", "properties": [ { "name": "cdx:npm:package:development", @@ -541,13 +532,21 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz" + "url": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git" + }, + { + "type": "website", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/deep-eql" } ], "hashes": [ { "alg": "SHA-512", - "content": "b8c2f6d63d8ae537cf1aeb4ac6e6fe33e9cb8d922b5a35d0e46af1e2509efe78a64e3f41e0beb7cc7a635cb978cb42f799c9b686d11021bd3aa021bfb337ab37" + "content": "73d87d75554c8a030f7386f04ef0b9771aada8967040f78fb168cf96948e9e88dba2bea91aa764e78d657c0ec0a8542be6907505176ad23b98f5d6fcd41c3217" } ], "licenses": [ @@ -559,12 +558,13 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-arm64-gnu@1.1.5", + "bom-ref": "@types/estree@1.0.8", "type": "library", - "name": "@rolldown/binding-linux-arm64-gnu", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-arm64-gnu@1.1.5", + "name": "@types/estree", + "version": "1.0.8", + "scope": "required", + "description": "TypeScript definitions for estree", + "purl": "pkg:npm/%40types/estree@1.0.8", "properties": [ { "name": "cdx:npm:package:development", @@ -573,14 +573,12 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git" + }, { - "alg": "SHA-512", - "content": "9dabd28ae4cca20be742866833fbfe977656a39d3f353c121d2ce1780071fd10a79943da2b0abda92a3806bd8e611b36d7e173f2e16a21364cdc7e28a4e074fd" + "type": "website", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree" } ], "licenses": [ @@ -592,12 +590,13 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-arm64-musl@1.1.5", + "bom-ref": "@types/node@26.1.1", "type": "library", - "name": "@rolldown/binding-linux-arm64-musl", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-arm64-musl@1.1.5", + "name": "@types/node", + "version": "26.1.1", + "scope": "required", + "description": "TypeScript definitions for node", + "purl": "pkg:npm/%40types/node@26.1.1", "properties": [ { "name": "cdx:npm:package:development", @@ -607,13 +606,21 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz" - } - ], + "url": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git" + }, + { + "type": "website", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node" + } + ], "hashes": [ { "alg": "SHA-512", - "content": "940af2a87ec8b5eced9825d05e4d1eb4a8f8c0143b1b1e52e8b8ca86c829f736fc2396ecbaf53fda5947d610d0723b057aa22ca2f315377dfdffca540c3057c4" + "content": "9f1024452564375634242d56f24cbf7d37e41ac32672b46c6f1fb75e8644fab30e5fbd642d84d5edf2d7a6ab9dd46a5ba4fe53b9f7d716a7d7edf1f61a065113" } ], "licenses": [ @@ -625,12 +632,14 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-ppc64-gnu@1.1.5", + "bom-ref": "@vitest/coverage-v8@4.1.10", "type": "library", - "name": "@rolldown/binding-linux-ppc64-gnu", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-ppc64-gnu@1.1.5", + "name": "@vitest/coverage-v8", + "version": "4.1.10", + "scope": "required", + "author": "Anthony Fu ", + "description": "V8 coverage provider for Vitest", + "purl": "pkg:npm/%40vitest/coverage-v8@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -640,13 +649,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://vitest.dev/guide/coverage" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "7ec2bfb0d067c730652f83b524dad96a4550c4fb29aa9103e5d2ed36c652f6838a9ad4a974d233c47da49289791d84d624de3bb04db4ced3093f17d9f3da863a" + "content": "20ce3d1e6b617af6e000ee1a9e9d61c2da13f7061ee7dc342d1d3482bf9e6a01c4f9927994ae2c2cf78ed2e6e0a097302e4e9d7a1537476e0df856c410a0dcf2" } ], "licenses": [ @@ -658,12 +679,13 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-s390x-gnu@1.1.5", + "bom-ref": "@vitest/expect@4.1.10", "type": "library", - "name": "@rolldown/binding-linux-s390x-gnu", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-s390x-gnu@1.1.5", + "name": "@vitest/expect", + "version": "4.1.10", + "scope": "required", + "description": "Jest's expect matchers as a Chai plugin", + "purl": "pkg:npm/%40vitest/expect@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -673,13 +695,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://vitest.dev/api/expect" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "80b61be0121a7657d33984f980ee74de7f334235df960ce90f415cc8a874333c77aea0992a71e825657dc5ed4a5d4279971d897dc487aff9a1cd2d0f0bf31c00" + "content": "62c0a7faa024d465a340e58512c11c2f680d434ce656642edd3d37a8fe94ca386d99db70b5bb88f830129ffee2401dc71935e4741c0675dcf13e59a2aa5e6f94" } ], "licenses": [ @@ -691,12 +725,13 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-x64-gnu@1.1.5", + "bom-ref": "@vitest/mocker@4.1.10", "type": "library", - "name": "@rolldown/binding-linux-x64-gnu", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-x64-gnu@1.1.5", + "name": "@vitest/mocker", + "version": "4.1.10", + "scope": "required", + "description": "Vitest module mocker implementation", + "purl": "pkg:npm/%40vitest/mocker@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -706,13 +741,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://github.com/vitest-dev/vitest/tree/main/packages/mocker" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "16372910a53227280782cd68e7455836f92de7eecb7bf544758b7402446990bdf7327c907f0afc979997c0c99f9936f230faf9bc92c2fbcfc677d8179f053541" + "content": "bf4c5a7b3b7e0ca12629f6b1835df795dcc00ebc0b19ded97b531f4104d87efb3c3aa648c1bc72c5a614462bf057bb16cb97ea9f7ac7e6e3ab4a9d3b6e9e383b" } ], "licenses": [ @@ -724,12 +771,13 @@ ] }, { - "bom-ref": "@rolldown/binding-linux-x64-musl@1.1.5", + "bom-ref": "@vitest/pretty-format@4.1.10", "type": "library", - "name": "@rolldown/binding-linux-x64-musl", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-linux-x64-musl@1.1.5", + "name": "@vitest/pretty-format", + "version": "4.1.10", + "scope": "required", + "description": "Fork of pretty-format with support for ESM", + "purl": "pkg:npm/%40vitest/pretty-format@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -739,13 +787,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://github.com/vitest-dev/vitest/tree/main/packages/pretty-format" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "31ef8f7cf2364cc78e424d206167cb419b5392dae6cdbafc7036e8a97f375ca73b52b8008b9e6017ed9d52459dc5dd7d9f9e44b2ca76c9e71af8ef4de6b0711e" + "content": "5b51ec8d21f831743d61f9a684b0282187f51d17de9100869e078881c7a2e8c3f941019651ed20928a5d6506950853be24324cac0246c1ae69469369bf2e0ff1" } ], "licenses": [ @@ -757,12 +817,13 @@ ] }, { - "bom-ref": "@rolldown/binding-openharmony-arm64@1.1.5", + "bom-ref": "@vitest/runner@4.1.10", "type": "library", - "name": "@rolldown/binding-openharmony-arm64", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-openharmony-arm64@1.1.5", + "name": "@vitest/runner", + "version": "4.1.10", + "scope": "required", + "description": "Vitest test runner", + "purl": "pkg:npm/%40vitest/runner@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -772,13 +833,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://vitest.dev/api/advanced/runner" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "c9ce56acbcd792ceb30907e7f4ec6bf293912b297fa45f908c7996fd0c77aaed28cabacd0becb624b4d4d44dab7166009b3967aa78165c7423cb9d55cd6ef457" + "content": "20a23a929207f8b9a944ea65c8bc0105a09f32039938cb328156ba040443e9a840d38551b8949ae8e6951bb911bd210c0fcef455df75ad24b0d1e7a0b56c8b1a" } ], "licenses": [ @@ -790,12 +863,13 @@ ] }, { - "bom-ref": "@rolldown/binding-wasm32-wasi@1.1.5", + "bom-ref": "@vitest/snapshot@4.1.10", "type": "library", - "name": "@rolldown/binding-wasm32-wasi", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-wasm32-wasi@1.1.5", + "name": "@vitest/snapshot", + "version": "4.1.10", + "scope": "required", + "description": "Vitest snapshot manager", + "purl": "pkg:npm/%40vitest/snapshot@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -805,13 +879,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://vitest.dev/guide/snapshot" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "55b4063d7d9be2be3c4c0308336723825b883351d8bad9b8a5c4c426c95eee210feec075745aad3cb0556dd2c0642c72d6dc4270fc5fe1015fe2ff2ebe5b4fa8" + "content": "c5191f393d6aa53022fd38b86352ed7d1737904baac46c39f5e3768cdf6945632d4bf5c37af7a485c152aaf42a8d434692c7e3309bb763ea09fb86299f639a27" } ], "licenses": [ @@ -823,12 +909,13 @@ ] }, { - "bom-ref": "@rolldown/binding-win32-arm64-msvc@1.1.5", + "bom-ref": "@vitest/spy@4.1.10", "type": "library", - "name": "@rolldown/binding-win32-arm64-msvc", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-win32-arm64-msvc@1.1.5", + "name": "@vitest/spy", + "version": "4.1.10", + "scope": "required", + "description": "Lightweight Jest compatible spy implementation", + "purl": "pkg:npm/%40vitest/spy@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -838,13 +925,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://vitest.dev/api/mock" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "807bfcda4eb7cf8aa9579f90d72ff5d8aacad25b5606e9150c8f2765c6d3ed3b7f665388570a696b39deab417dde80f14e8dc88003040c8a108771369fa995b3" + "content": "3cb7ff520be8ab9c0efdbe2bc18091d61d8f488757cfbc2791014c894a4b76d33b97aa6a545710201107c93d7e97e723ee637001f647cea5ea0f28ef3aec1b0f" } ], "licenses": [ @@ -856,12 +955,13 @@ ] }, { - "bom-ref": "@rolldown/binding-win32-x64-msvc@1.1.5", + "bom-ref": "@vitest/utils@4.1.10", "type": "library", - "name": "@rolldown/binding-win32-x64-msvc", - "version": "1.1.5", - "scope": "optional", - "purl": "pkg:npm/%40rolldown/binding-win32-x64-msvc@1.1.5", + "name": "@vitest/utils", + "version": "4.1.10", + "scope": "required", + "description": "Shared Vitest utility functions", + "purl": "pkg:npm/%40vitest/utils@4.1.10", "properties": [ { "name": "cdx:npm:package:development", @@ -871,13 +971,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz" + "url": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://github.com/vitest-dev/vitest/tree/main/packages/utils" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "b5366e0c13f0f39b443793d08b5a67101cc3cb4678f47b5270b01b0f9b7a872794f7603de69456692330d466598bf470812814225d31ad2957213ffd433bd848" + "content": "7f2f5a9bf1d6c5b686b7f49ac2ba7dd2fb7a63a8d0c1fd515fbedccf7bb0a09c0954c962fded4813044f9cc349eef29f3d3c28d1d89789f9293efc07f6fee718" } ], "licenses": [ @@ -889,28 +1001,23 @@ ] }, { - "bom-ref": "@rolldown/pluginutils@1.0.1", + "bom-ref": "@vscode/tree-sitter-wasm@0.3.1", "type": "library", - "name": "@rolldown/pluginutils", - "version": "1.0.1", + "name": "@vscode/tree-sitter-wasm", + "version": "0.3.1", "scope": "required", - "purl": "pkg:npm/%40rolldown/pluginutils@1.0.1", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], + "author": "Visual Studio Code Team", + "description": "Pre-built WASM files for Tree-Sitter and Tree-Sitter languages that VS Code uses", + "purl": "pkg:npm/%40vscode/tree-sitter-wasm@0.3.1", + "properties": [], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "https://github.com/Microsoft/vscode-tree-sitter-wasm.git" + }, { - "alg": "SHA-512", - "content": "da3f5b1ade4987c863faf3ed8333ed97bda3d324711c0cae9a8a3a4cd7c08ec2c1d3852da52bcf6cf703701331cfb9fef42601d1cd46c501716118368e29aa1b" + "type": "issue-tracker", + "url": "https://github.com/Microsoft/vscode-tree-sitter-wasm/issues" } ], "licenses": [ @@ -922,12 +1029,14 @@ ] }, { - "bom-ref": "@standard-schema/spec@1.1.0", + "bom-ref": "ajv@8.20.0", "type": "library", - "name": "@standard-schema/spec", - "version": "1.1.0", + "name": "ajv", + "version": "8.20.0", "scope": "required", - "purl": "pkg:npm/%40standard-schema/spec@1.1.0", + "author": "Evgeny Poberezkin", + "description": "Another JSON Schema Validator", + "purl": "pkg:npm/ajv@8.20.0", "properties": [ { "name": "cdx:npm:package:development", @@ -937,13 +1046,17 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz" + "url": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz" + }, + { + "type": "website", + "url": "https://ajv.js.org" } ], "hashes": [ { "alg": "SHA-512", - "content": "976685cb98c02e19e21b91e0aab0fa8d72e2feb516acabea37fa89c7aca826c80a85b95577e8aaa94e110976af9bf8cf8adc83a394c2bca327a632a73ab8b2d3" + "content": "4e16e58be3a53a3fa230f60505505f2773a60809da4b2367e0cd6fcfd4fa1a46b926df5b6bf1c8479ea3a32eb9b58ea4c7f142179557341f35f58eff194ac118" } ], "licenses": [ @@ -955,12 +1068,14 @@ ] }, { - "bom-ref": "@tybys/wasm-util@0.10.3", + "bom-ref": "ajv-formats@3.0.1", "type": "library", - "name": "@tybys/wasm-util", - "version": "0.10.3", - "scope": "optional", - "purl": "pkg:npm/%40tybys/wasm-util@0.10.3", + "name": "ajv-formats", + "version": "3.0.1", + "scope": "required", + "author": "Evgeny Poberezkin", + "description": "Format validation for Ajv v7+", + "purl": "pkg:npm/ajv-formats@3.0.1", "properties": [ { "name": "cdx:npm:package:development", @@ -970,13 +1085,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz" + "url": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/ajv-validator/ajv-formats.git" + }, + { + "type": "website", + "url": "https://github.com/ajv-validator/ajv-formats#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/ajv-validator/ajv-formats/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "1777e8d4c62b44960bdf3111d0e50e9a4bad8ebd55a76de6eceb12829ee7ab848fe8ea97e82ff9e971483c09796eddf3681463996ed21b3deeffa2f016961c3a" + "content": "f2252a979d04511fae51c7514371c3a9ae84572a3776870bf20e5627714d7169aeeb621b90652e7bfa44c8b056f1518a2ae7133e0a9e92ce1f214d43038ca8c1" } ], "licenses": [ @@ -988,12 +1115,14 @@ ] }, { - "bom-ref": "@types/chai@5.2.3", + "bom-ref": "assertion-error@2.0.1", "type": "library", - "name": "@types/chai", - "version": "5.2.3", + "name": "assertion-error", + "version": "2.0.1", "scope": "required", - "purl": "pkg:npm/%40types/chai@5.2.3", + "author": "Jake Luer (http://qualiancy.com)", + "description": "Error constructor for test and validation frameworks that implements standardized AssertionError specification.", + "purl": "pkg:npm/assertion-error@2.0.1", "properties": [ { "name": "cdx:npm:package:development", @@ -1003,13 +1132,17 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz" + "url": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" + }, + { + "type": "vcs", + "url": "git@github.com:chaijs/assertion-error.git" } ], "hashes": [ { "alg": "SHA-512", - "content": "330e79f28780f5f15bbfae7fcb8987b570ecf5b3e714c6402ff8f174f154a4e1c72175fdd667201076d2e4b6a1afea7064547c03b19095e456788e9c1850b650" + "content": "2338bc45071f7ea09e3558058a02a58b5b2c92521ba479c261ce809275c662807a82b26ac9e6f2ee3bf5d895108264c09c80e76dc935bb192c4f87733773d604" } ], "licenses": [ @@ -1021,12 +1154,14 @@ ] }, { - "bom-ref": "@types/deep-eql@4.0.2", + "bom-ref": "ast-v8-to-istanbul@1.0.0", "type": "library", - "name": "@types/deep-eql", - "version": "4.0.2", + "name": "ast-v8-to-istanbul", + "version": "1.0.0", "scope": "required", - "purl": "pkg:npm/%40types/deep-eql@4.0.2", + "author": "Ari Perkkiƶ ", + "description": "AST-aware v8-to-istanbul", + "purl": "pkg:npm/ast-v8-to-istanbul@1.0.0", "properties": [ { "name": "cdx:npm:package:development", @@ -1035,14 +1170,12 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+https://github.com/AriPerkkio/ast-v8-to-istanbul.git" + }, { - "alg": "SHA-512", - "content": "73d87d75554c8a030f7386f04ef0b9771aada8967040f78fb168cf96948e9e88dba2bea91aa764e78d657c0ec0a8542be6907505176ad23b98f5d6fcd41c3217" + "type": "website", + "url": "https://github.com/AriPerkkio/ast-v8-to-istanbul" } ], "licenses": [ @@ -1054,19 +1187,39 @@ ] }, { - "bom-ref": "@types/estree@1.0.8", + "bom-ref": "base64-js@1.5.1", "type": "library", - "name": "@types/estree", - "version": "1.0.8", + "name": "base64-js", + "version": "1.5.1", "scope": "required", - "purl": "pkg:npm/%40types/estree@1.0.8", - "properties": [ + "author": "T. Jameson Little ", + "description": "Base64 encoding/decoding in pure JS", + "purl": "pkg:npm/base64-js@1.5.1", + "properties": [], + "externalReferences": [ { - "name": "cdx:npm:package:development", - "value": "true" + "type": "distribution", + "url": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/beatgammit/base64-js.git" + }, + { + "type": "website", + "url": "https://github.com/beatgammit/base64-js" + }, + { + "type": "issue-tracker", + "url": "https://github.com/beatgammit/base64-js/issues" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "00aa5a6251e7f2de1255b3870b2f9be7e28a82f478bebb03f2f6efadb890269b3b7ca0d3923903af2ea38b4ad42630b49336cd78f2f0cf1abc8b2a68e35a9e58" } ], - "externalReferences": [], "licenses": [ { "license": { @@ -1076,28 +1229,37 @@ ] }, { - "bom-ref": "@types/node@26.1.1", + "bom-ref": "buffer@6.0.3", "type": "library", - "name": "@types/node", - "version": "26.1.1", + "name": "buffer", + "version": "6.0.3", "scope": "required", - "purl": "pkg:npm/%40types/node@26.1.1", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], + "author": "Feross Aboukhadijeh", + "description": "Node.js Buffer API, for the browser", + "purl": "pkg:npm/buffer@6.0.3", + "properties": [], "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz" + "url": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/feross/buffer.git" + }, + { + "type": "website", + "url": "https://github.com/feross/buffer" + }, + { + "type": "issue-tracker", + "url": "https://github.com/feross/buffer/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "9f1024452564375634242d56f24cbf7d37e41ac32672b46c6f1fb75e8644fab30e5fbd642d84d5edf2d7a6ab9dd46a5ba4fe53b9f7d716a7d7edf1f61a065113" + "content": "153882a4dc6dc226591c465b71b4c87198c44552029fdcaafe90c591397de7f031cc3d6768172d37b60eebcae233f80b48363bb1dacc6f2f21a1f00362ebaa38" } ], "licenses": [ @@ -1109,12 +1271,14 @@ ] }, { - "bom-ref": "@vitest/coverage-v8@4.1.10", + "bom-ref": "chai@6.2.2", "type": "library", - "name": "@vitest/coverage-v8", - "version": "4.1.10", + "name": "chai", + "version": "6.2.2", "scope": "required", - "purl": "pkg:npm/%40vitest/coverage-v8@4.1.10", + "author": "Jake Luer ", + "description": "BDD/TDD assertion library for node.js and the browser. Test framework agnostic.", + "purl": "pkg:npm/chai@6.2.2", "properties": [ { "name": "cdx:npm:package:development", @@ -1124,13 +1288,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz" + "url": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/chaijs/chai" + }, + { + "type": "website", + "url": "http://chaijs.com" + }, + { + "type": "issue-tracker", + "url": "https://github.com/chaijs/chai/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "20ce3d1e6b617af6e000ee1a9e9d61c2da13f7061ee7dc342d1d3482bf9e6a01c4f9927994ae2c2cf78ed2e6e0a097302e4e9d7a1537476e0df856c410a0dcf2" + "content": "3543d196e39f3a24ca04abd63ed483e0f845bd60aa3a2d01192b4d5ace7b5fd8eced7193a6b4a6168cf9174b56851e163e335e47d8d7a9d0bbfd4a522539e546" } ], "licenses": [ @@ -1142,12 +1318,14 @@ ] }, { - "bom-ref": "@vitest/expect@4.1.10", + "bom-ref": "convert-source-map@2.0.0", "type": "library", - "name": "@vitest/expect", - "version": "4.1.10", + "name": "convert-source-map", + "version": "2.0.0", "scope": "required", - "purl": "pkg:npm/%40vitest/expect@4.1.10", + "author": "Thorsten Lorenz", + "description": "Converts a source-map from/to different formats and allows adding/changing properties.", + "purl": "pkg:npm/convert-source-map@2.0.0", "properties": [ { "name": "cdx:npm:package:development", @@ -1157,13 +1335,21 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz" + "url": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/thlorenz/convert-source-map.git" + }, + { + "type": "website", + "url": "https://github.com/thlorenz/convert-source-map" } ], "hashes": [ { "alg": "SHA-512", - "content": "62c0a7faa024d465a340e58512c11c2f680d434ce656642edd3d37a8fe94ca386d99db70b5bb88f830129ffee2401dc71935e4741c0675dcf13e59a2aa5e6f94" + "content": "2afa78e7d1eb576144275080b22d4abbe318de46ac1f5f53172913cf6c5698c7aae9b936354dd75ef7c9f90eb59b4c64b56c2dfb51d261fdc966c4e6b3769126" } ], "licenses": [ @@ -1175,12 +1361,14 @@ ] }, { - "bom-ref": "@vitest/mocker@4.1.10", + "bom-ref": "detect-libc@2.1.2", "type": "library", - "name": "@vitest/mocker", - "version": "4.1.10", + "name": "detect-libc", + "version": "2.1.2", "scope": "required", - "purl": "pkg:npm/%40vitest/mocker@4.1.10", + "author": "Lovell Fuller ", + "description": "Node.js module to detect the C standard library (libc) implementation family and version", + "purl": "pkg:npm/detect-libc@2.1.2", "properties": [ { "name": "cdx:npm:package:development", @@ -1189,31 +1377,27 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "bf4c5a7b3b7e0ca12629f6b1835df795dcc00ebc0b19ded97b531f4104d87efb3c3aa648c1bc72c5a614462bf057bb16cb97ea9f7ac7e6e3ab4a9d3b6e9e383b" + "type": "vcs", + "url": "git://github.com/lovell/detect-libc.git" } ], "licenses": [ { "license": { - "id": "MIT" + "id": "Apache-2.0" } } ] }, { - "bom-ref": "@vitest/pretty-format@4.1.10", + "bom-ref": "es-module-lexer@2.1.0", "type": "library", - "name": "@vitest/pretty-format", - "version": "4.1.10", + "name": "es-module-lexer", + "version": "2.1.0", "scope": "required", - "purl": "pkg:npm/%40vitest/pretty-format@4.1.10", + "author": "Guy Bedford", + "description": "Lexes ES modules returning their import/export metadata", + "purl": "pkg:npm/es-module-lexer@2.1.0", "properties": [ { "name": "cdx:npm:package:development", @@ -1222,14 +1406,16 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+https://github.com/guybedford/es-module-lexer.git" + }, { - "alg": "SHA-512", - "content": "5b51ec8d21f831743d61f9a684b0282187f51d17de9100869e078881c7a2e8c3f941019651ed20928a5d6506950853be24324cac0246c1ae69469369bf2e0ff1" + "type": "website", + "url": "https://github.com/guybedford/es-module-lexer#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/guybedford/es-module-lexer/issues" } ], "licenses": [ @@ -1241,12 +1427,14 @@ ] }, { - "bom-ref": "@vitest/runner@4.1.10", + "bom-ref": "estree-walker@3.0.3", "type": "library", - "name": "@vitest/runner", - "version": "4.1.10", + "name": "estree-walker", + "version": "3.0.3", "scope": "required", - "purl": "pkg:npm/%40vitest/runner@4.1.10", + "author": "Rich Harris", + "description": "Traverse an ESTree-compliant AST", + "purl": "pkg:npm/estree-walker@3.0.3", "properties": [ { "name": "cdx:npm:package:development", @@ -1255,14 +1443,8 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "20a23a929207f8b9a944ea65c8bc0105a09f32039938cb328156ba040443e9a840d38551b8949ae8e6951bb911bd210c0fcef455df75ad24b0d1e7a0b56c8b1a" + "type": "vcs", + "url": "https://github.com/Rich-Harris/estree-walker" } ], "licenses": [ @@ -1274,12 +1456,12 @@ ] }, { - "bom-ref": "@vitest/snapshot@4.1.10", + "bom-ref": "expect-type@1.3.0", "type": "library", - "name": "@vitest/snapshot", - "version": "4.1.10", + "name": "expect-type", + "version": "1.3.0", "scope": "required", - "purl": "pkg:npm/%40vitest/snapshot@4.1.10", + "purl": "pkg:npm/expect-type@1.3.0", "properties": [ { "name": "cdx:npm:package:development", @@ -1288,31 +1470,31 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "https://github.com/mmkal/expect-type.git" + }, { - "alg": "SHA-512", - "content": "c5191f393d6aa53022fd38b86352ed7d1737904baac46c39f5e3768cdf6945632d4bf5c37af7a485c152aaf42a8d434692c7e3309bb763ea09fb86299f639a27" + "type": "website", + "url": "https://github.com/mmkal/expect-type#readme" } ], "licenses": [ { "license": { - "id": "MIT" + "id": "Apache-2.0" } } ] }, { - "bom-ref": "@vitest/spy@4.1.10", + "bom-ref": "fast-deep-equal@3.1.3", "type": "library", - "name": "@vitest/spy", - "version": "4.1.10", + "name": "fast-deep-equal", + "version": "3.1.3", "scope": "required", - "purl": "pkg:npm/%40vitest/spy@4.1.10", + "author": "Evgeny Poberezkin", + "description": "Fast deep equal", + "purl": "pkg:npm/fast-deep-equal@3.1.3", "properties": [ { "name": "cdx:npm:package:development", @@ -1322,13 +1504,25 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz" + "url": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/epoberezkin/fast-deep-equal.git" + }, + { + "type": "website", + "url": "https://github.com/epoberezkin/fast-deep-equal#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/epoberezkin/fast-deep-equal/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "3cb7ff520be8ab9c0efdbe2bc18091d61d8f488757cfbc2791014c894a4b76d33b97aa6a545710201107c93d7e97e723ee637001f647cea5ea0f28ef3aec1b0f" + "content": "7f7a90f68432f63d808417bf1fd542f75c0b98a042094fe00ce9ca340606e61b303bb04b2a3d3d1dce4760dcfd70623efb19690c22200da8ad56cd3701347ce1" } ], "licenses": [ @@ -1340,12 +1534,14 @@ ] }, { - "bom-ref": "@vitest/utils@4.1.10", + "bom-ref": "fast-uri@3.1.2", "type": "library", - "name": "@vitest/utils", - "version": "4.1.10", + "name": "fast-uri", + "version": "3.1.2", "scope": "required", - "purl": "pkg:npm/%40vitest/utils@4.1.10", + "author": "Vincent Le Goff (https://github.com/zekth)", + "description": "Dependency-free RFC 3986 URI toolbox", + "purl": "pkg:npm/fast-uri@3.1.2", "properties": [ { "name": "cdx:npm:package:development", @@ -1355,47 +1551,44 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz" + "url": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/fastify/fast-uri.git" + }, + { + "type": "website", + "url": "https://github.com/fastify/fast-uri" + }, + { + "type": "issue-tracker", + "url": "https://github.com/fastify/fast-uri/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "7f2f5a9bf1d6c5b686b7f49ac2ba7dd2fb7a63a8d0c1fd515fbedccf7bb0a09c0954c962fded4813044f9cc349eef29f3d3c28d1d89789f9293efc07f6fee718" + "content": "ad58dfec0ac6dcb4e4f854ba630f355750cbb999756d16cdadebfa4e677ff516aba1e791449840b7b8e0ffa605c5bbc04175026af4a86613cf8faa0ec7ee4a8d" } ], "licenses": [ { "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "@vscode/tree-sitter-wasm@0.3.1", - "type": "library", - "name": "@vscode/tree-sitter-wasm", - "version": "0.3.1", - "scope": "required", - "purl": "pkg:npm/%40vscode/tree-sitter-wasm@0.3.1", - "properties": [], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" + "id": "BSD-3-Clause" } } ] }, { - "bom-ref": "ajv@8.20.0", + "bom-ref": "fdir@6.5.0", "type": "library", - "name": "ajv", - "version": "8.20.0", + "name": "fdir", + "version": "6.5.0", "scope": "required", - "purl": "pkg:npm/ajv@8.20.0", + "author": "thecodrr ", + "description": "The fastest directory crawler & globbing alternative to glob, fast-glob, & tiny-glob. Crawls 1m files in < 1s", + "purl": "pkg:npm/fdir@6.5.0", "properties": [ { "name": "cdx:npm:package:development", @@ -1404,14 +1597,16 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+https://github.com/thecodrr/fdir.git" + }, { - "alg": "SHA-512", - "content": "4e16e58be3a53a3fa230f60505505f2773a60809da4b2367e0cd6fcfd4fa1a46b926df5b6bf1c8479ea3a32eb9b58ea4c7f142179557341f35f58eff194ac118" + "type": "website", + "url": "https://github.com/thecodrr/fdir#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/thecodrr/fdir/issues" } ], "licenses": [ @@ -1423,61 +1618,33 @@ ] }, { - "bom-ref": "ajv-formats@3.0.1", + "bom-ref": "fflate@0.8.3", "type": "library", - "name": "ajv-formats", - "version": "3.0.1", + "name": "fflate", + "version": "0.8.3", "scope": "required", - "purl": "pkg:npm/ajv-formats@3.0.1", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ + "author": "Arjun Barrett ", + "description": "High performance (de)compression in an 8kB package", + "purl": "pkg:npm/fflate@0.8.3", + "properties": [], + "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "f2252a979d04511fae51c7514371c3a9ae84572a3776870bf20e5627714d7169aeeb621b90652e7bfa44c8b056f1518a2ae7133e0a9e92ce1f214d43038ca8c1" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "assertion-error@2.0.1", - "type": "library", - "name": "assertion-error", - "version": "2.0.1", - "scope": "required", - "purl": "pkg:npm/assertion-error@2.0.1", - "properties": [ + "url": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz" + }, { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ + "type": "website", + "url": "https://101arrowz.github.io/fflate" + }, { - "type": "distribution", - "url": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" + "type": "issue-tracker", + "url": "https://github.com/101arrowz/fflate/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "2338bc45071f7ea09e3558058a02a58b5b2c92521ba479c261ce809275c662807a82b26ac9e6f2ee3bf5d895108264c09c80e76dc935bb192c4f87733773d604" + "content": "b5b64db89acbc06529df3b2106d772e16f8e47166e221f1ae629722044030b9ad8d5fdd4db424caf2d0b977581cd4e7c1192ac12e2455e16f9830bfc0ac3ef80" } ], "licenses": [ @@ -1489,106 +1656,41 @@ ] }, { - "bom-ref": "ast-v8-to-istanbul@1.0.0", + "bom-ref": "fsevents@2.3.3", "type": "library", - "name": "ast-v8-to-istanbul", - "version": "1.0.0", - "scope": "required", - "purl": "pkg:npm/ast-v8-to-istanbul@1.0.0", + "name": "fsevents", + "version": "2.3.3", + "scope": "optional", + "description": "Native Access to MacOS FSEvents", + "purl": "pkg:npm/fsevents@2.3.3", "properties": [ { "name": "cdx:npm:package:development", "value": "true" } ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "base64-js@1.5.1", - "type": "library", - "name": "base64-js", - "version": "1.5.1", - "scope": "required", - "purl": "pkg:npm/base64-js@1.5.1", - "properties": [], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "00aa5a6251e7f2de1255b3870b2f9be7e28a82f478bebb03f2f6efadb890269b3b7ca0d3923903af2ea38b4ad42630b49336cd78f2f0cf1abc8b2a68e35a9e58" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "buffer@6.0.3", - "type": "library", - "name": "buffer", - "version": "6.0.3", - "scope": "required", - "purl": "pkg:npm/buffer@6.0.3", - "properties": [], "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "153882a4dc6dc226591c465b71b4c87198c44552029fdcaafe90c591397de7f031cc3d6768172d37b60eebcae233f80b48363bb1dacc6f2f21a1f00362ebaa38" - } - ], - "licenses": [ + "url": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + }, { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "chai@6.2.2", - "type": "library", - "name": "chai", - "version": "6.2.2", - "scope": "required", - "purl": "pkg:npm/chai@6.2.2", - "properties": [ + "type": "vcs", + "url": "https://github.com/fsevents/fsevents.git" + }, { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ + "type": "website", + "url": "https://github.com/fsevents/fsevents" + }, { - "type": "distribution", - "url": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz" + "type": "issue-tracker", + "url": "https://github.com/fsevents/fsevents/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "3543d196e39f3a24ca04abd63ed483e0f845bd60aa3a2d01192b4d5ace7b5fd8eced7193a6b4a6168cf9174b56851e163e335e47d8d7a9d0bbfd4a522539e546" + "content": "e71a037d7f9f2fb7da0139da82658fa5b16dc21fd1efb5a630caaa1c64bae42defbc1d181eb805f81d58999df8e35b4c8f99fade4d36d765cda09c339617df43" } ], "licenses": [ @@ -1600,96 +1702,29 @@ ] }, { - "bom-ref": "convert-source-map@2.0.0", + "bom-ref": "gpt-tokenizer@3.4.0", "type": "library", - "name": "convert-source-map", - "version": "2.0.0", + "name": "gpt-tokenizer", + "version": "3.4.0", "scope": "required", - "purl": "pkg:npm/convert-source-map@2.0.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], + "author": "Bazyli Brzoska (https://github.com/niieani)", + "description": "A pure JavaScript implementation of a BPE tokenizer (Encoder/Decoder) for GPT-2 / GPT-3 / GPT-4 and other OpenAI models", + "purl": "pkg:npm/gpt-tokenizer@3.4.0", + "properties": [], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "2afa78e7d1eb576144275080b22d4abbe318de46ac1f5f53172913cf6c5698c7aae9b936354dd75ef7c9f90eb59b4c64b56c2dfb51d261fdc966c4e6b3769126" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "detect-libc@2.1.2", - "type": "library", - "name": "detect-libc", - "version": "2.1.2", - "scope": "required", - "purl": "pkg:npm/detect-libc@2.1.2", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ] - }, - { - "bom-ref": "es-module-lexer@2.1.0", - "type": "library", - "name": "es-module-lexer", - "version": "2.1.0", - "scope": "required", - "purl": "pkg:npm/es-module-lexer@2.1.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ + "type": "vcs", + "url": "https://github.com/niieani/gpt-tokenizer.git" + }, { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "estree-walker@3.0.3", - "type": "library", - "name": "estree-walker", - "version": "3.0.3", - "scope": "required", - "purl": "pkg:npm/estree-walker@3.0.3", - "properties": [ + "type": "website", + "url": "https://github.com/niieani/gpt-tokenizer#readme" + }, { - "name": "cdx:npm:package:development", - "value": "true" + "type": "issue-tracker", + "url": "https://github.com/niieani/gpt-tokenizer/issues" } ], - "externalReferences": [], "licenses": [ { "license": { @@ -1699,492 +1734,38 @@ ] }, { - "bom-ref": "expect-type@1.3.0", - "type": "library", - "name": "expect-type", - "version": "1.3.0", - "scope": "required", - "purl": "pkg:npm/expect-type@1.3.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ] - }, - { - "bom-ref": "fast-deep-equal@3.1.3", + "bom-ref": "has-flag@4.0.0", "type": "library", - "name": "fast-deep-equal", - "version": "3.1.3", - "scope": "required", - "purl": "pkg:npm/fast-deep-equal@3.1.3", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "7f7a90f68432f63d808417bf1fd542f75c0b98a042094fe00ce9ca340606e61b303bb04b2a3d3d1dce4760dcfd70623efb19690c22200da8ad56cd3701347ce1" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "fast-uri@3.1.2", - "type": "library", - "name": "fast-uri", - "version": "3.1.2", - "scope": "required", - "purl": "pkg:npm/fast-uri@3.1.2", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "ad58dfec0ac6dcb4e4f854ba630f355750cbb999756d16cdadebfa4e677ff516aba1e791449840b7b8e0ffa605c5bbc04175026af4a86613cf8faa0ec7ee4a8d" - } - ], - "licenses": [ - { - "license": { - "id": "BSD-3-Clause" - } - } - ] - }, - { - "bom-ref": "fdir@6.5.0", - "type": "library", - "name": "fdir", - "version": "6.5.0", - "scope": "required", - "purl": "pkg:npm/fdir@6.5.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "fflate@0.8.3", - "type": "library", - "name": "fflate", - "version": "0.8.3", - "scope": "required", - "purl": "pkg:npm/fflate@0.8.3", - "properties": [], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "b5b64db89acbc06529df3b2106d772e16f8e47166e221f1ae629722044030b9ad8d5fdd4db424caf2d0b977581cd4e7c1192ac12e2455e16f9830bfc0ac3ef80" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "fsevents@2.3.3", - "type": "library", - "name": "fsevents", - "version": "2.3.3", - "scope": "optional", - "purl": "pkg:npm/fsevents@2.3.3", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "e71a037d7f9f2fb7da0139da82658fa5b16dc21fd1efb5a630caaa1c64bae42defbc1d181eb805f81d58999df8e35b4c8f99fade4d36d765cda09c339617df43" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "gpt-tokenizer@3.4.0", - "type": "library", - "name": "gpt-tokenizer", - "version": "3.4.0", - "scope": "required", - "purl": "pkg:npm/gpt-tokenizer@3.4.0", - "properties": [], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "has-flag@4.0.0", - "type": "library", - "name": "has-flag", - "version": "4.0.0", - "scope": "required", - "purl": "pkg:npm/has-flag@4.0.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "html-escaper@2.0.2", - "type": "library", - "name": "html-escaper", - "version": "2.0.2", - "scope": "required", - "purl": "pkg:npm/html-escaper@2.0.2", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "ieee754@1.2.1", - "type": "library", - "name": "ieee754", - "version": "1.2.1", - "scope": "required", - "purl": "pkg:npm/ieee754@1.2.1", - "properties": [], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "75ccaa843bd7d42e3a95765c56a0a92be16d31141574830debf0dfe63b36ce8b94b2a1bb23ab05c62b480beeca60adbd29d5ce2c776ef732f8b059e85509ea68" - } - ], - "licenses": [ - { - "license": { - "id": "BSD-3-Clause" - } - } - ] - }, - { - "bom-ref": "istanbul-lib-coverage@3.2.2", - "type": "library", - "name": "istanbul-lib-coverage", - "version": "3.2.2", - "scope": "required", - "purl": "pkg:npm/istanbul-lib-coverage@3.2.2", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "BSD-3-Clause" - } - } - ] - }, - { - "bom-ref": "istanbul-lib-report@3.0.1", - "type": "library", - "name": "istanbul-lib-report", - "version": "3.0.1", - "scope": "required", - "purl": "pkg:npm/istanbul-lib-report@3.0.1", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "BSD-3-Clause" - } - } - ] - }, - { - "bom-ref": "istanbul-reports@3.2.0", - "type": "library", - "name": "istanbul-reports", - "version": "3.2.0", - "scope": "required", - "purl": "pkg:npm/istanbul-reports@3.2.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "BSD-3-Clause" - } - } - ] - }, - { - "bom-ref": "js-tokens@10.0.0", - "type": "library", - "name": "js-tokens", - "version": "10.0.0", - "scope": "required", - "purl": "pkg:npm/js-tokens@10.0.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "json-schema-traverse@1.0.0", - "type": "library", - "name": "json-schema-traverse", - "version": "1.0.0", - "scope": "required", - "purl": "pkg:npm/json-schema-traverse@1.0.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "34cf3f3fd9f75e35e12199f594b86415a0024ce5114178d6855e0103f4673aff31be0aadaa9017f483b89914314b1d51968e2dab37aa6f4b0e96bb9a3b2dddba" - } - ], - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ] - }, - { - "bom-ref": "lightningcss@1.32.0", - "type": "library", - "name": "lightningcss", - "version": "1.32.0", - "scope": "required", - "purl": "pkg:npm/lightningcss@1.32.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "357601ce29cdadb95fada3c6cab6cfa03d7d0b587d95f23fd66ce0598bd75137b8d781b3fd7d450f65c165264ceeb453acc03c24bdceb4068689fac82a1439c9" - } - ], - "licenses": [ - { - "license": { - "id": "MPL-2.0" - } - } - ] - }, - { - "bom-ref": "lightningcss-android-arm64@1.32.0", - "type": "library", - "name": "lightningcss-android-arm64", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-android-arm64@1.32.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "60aeff0a54ede2400ad2fa3ac375fe3e79b40f671fdaf3c76e139776836d8b519ad1a9753f84c1661c23013be33702c40429cabe325cda34201d71f4344c2502" - } - ], - "licenses": [ - { - "license": { - "id": "MPL-2.0" - } - } - ] - }, - { - "bom-ref": "lightningcss-darwin-arm64@1.32.0", - "type": "library", - "name": "lightningcss-darwin-arm64", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-darwin-arm64@1.32.0", + "name": "has-flag", + "version": "4.0.0", + "scope": "required", + "author": "Sindre Sorhus", + "description": "Check if argv has a specific flag", + "purl": "pkg:npm/has-flag@4.0.0", "properties": [ { "name": "cdx:npm:package:development", "value": "true" } ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "473786f49bb96da83606fd7f97095526f044deae93b57b247592cb0b27e0e69b7e1cbcfd06a94808eecb64ced51cd4d39ffe4f4611c50528e4e65738726b1c3d" - } - ], + "externalReferences": [], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "MIT" } } ] }, { - "bom-ref": "lightningcss-darwin-x64@1.32.0", + "bom-ref": "html-escaper@2.0.2", "type": "library", - "name": "lightningcss-darwin-x64", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-darwin-x64@1.32.0", + "name": "html-escaper", + "version": "2.0.2", + "scope": "required", + "author": "Andrea Giammarchi", + "description": "fast and safe way to escape and unescape &<>'\" chars", + "purl": "pkg:npm/html-escaper@2.0.2", "properties": [ { "name": "cdx:npm:package:development", @@ -2193,64 +1774,69 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "https://github.com/WebReflection/html-escaper.git" + }, { - "alg": "SHA-512", - "content": "53e42c069da6fecdb0aa95184ffeb09e56a07596ed65d9dd4a6badfcd26a94270c2d35a9e66b82ac80fe2b9509ea3a83d8116c85e8c26179e23c36cd87bdd5f3" + "type": "website", + "url": "https://github.com/WebReflection/html-escaper" + }, + { + "type": "issue-tracker", + "url": "https://github.com/WebReflection/html-escaper/issues" } ], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "MIT" } } ] }, { - "bom-ref": "lightningcss-freebsd-x64@1.32.0", + "bom-ref": "ieee754@1.2.1", "type": "library", - "name": "lightningcss-freebsd-x64", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-freebsd-x64@1.32.0", - "properties": [ - { - "name": "cdx:npm:package:development", - "value": "true" - } - ], + "name": "ieee754", + "version": "1.2.1", + "scope": "required", + "author": "Feross Aboukhadijeh", + "description": "Read/write IEEE754 floating point numbers from/to a Buffer or array-like object", + "purl": "pkg:npm/ieee754@1.2.1", + "properties": [], "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz" + "url": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/feross/ieee754.git" } ], "hashes": [ { "alg": "SHA-512", - "content": "2424e281e74492c664ded1d34ed86731d55f19feb5164cbc262d84e188d44c4417d78c62cbf953cd79eed6fc2265eddb61ed2af92a6c487fc24de0d72ba5878a" + "content": "75ccaa843bd7d42e3a95765c56a0a92be16d31141574830debf0dfe63b36ce8b94b2a1bb23ab05c62b480beeca60adbd29d5ce2c776ef732f8b059e85509ea68" } ], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "BSD-3-Clause" } } ] }, { - "bom-ref": "lightningcss-linux-arm-gnueabihf@1.32.0", + "bom-ref": "istanbul-lib-coverage@3.2.2", "type": "library", - "name": "lightningcss-linux-arm-gnueabihf", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-linux-arm-gnueabihf@1.32.0", + "name": "istanbul-lib-coverage", + "version": "3.2.2", + "scope": "required", + "author": "Krishnan Anantheswaran ", + "description": "Data library for istanbul coverage objects", + "purl": "pkg:npm/istanbul-lib-coverage@3.2.2", "properties": [ { "name": "cdx:npm:package:development", @@ -2259,31 +1845,35 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+ssh://git@github.com/istanbuljs/istanbuljs.git" + }, { - "alg": "SHA-512", - "content": "c7aae79e945ad862f4cd03a4b7aaedb376033f376e2e95afc0017a10c8571556570f8b4fac190416acc6a30cc2b085ac3e3a922beb723443835015de7951d293" + "type": "website", + "url": "https://istanbul.js.org/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/istanbuljs/istanbuljs/issues" } ], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "BSD-3-Clause" } } ] }, { - "bom-ref": "lightningcss-linux-arm64-gnu@1.32.0", + "bom-ref": "istanbul-lib-report@3.0.1", "type": "library", - "name": "lightningcss-linux-arm64-gnu", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-linux-arm64-gnu@1.32.0", + "name": "istanbul-lib-report", + "version": "3.0.1", + "scope": "required", + "author": "Krishnan Anantheswaran ", + "description": "Base reporting library for istanbul", + "purl": "pkg:npm/istanbul-lib-report@3.0.1", "properties": [ { "name": "cdx:npm:package:development", @@ -2292,31 +1882,35 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+ssh://git@github.com/istanbuljs/istanbuljs.git" + }, { - "alg": "SHA-512", - "content": "d279ccca8c8e2d12577db30e8a569245c2c7dc9c39cfd1c33467d3fe0c023e06838e7c748bcc3bbc1cc52c54757fa08c2ca17c8156de6e69143777dafe4409a5" + "type": "website", + "url": "https://istanbul.js.org/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/istanbuljs/istanbuljs/issues" } ], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "BSD-3-Clause" } } ] }, { - "bom-ref": "lightningcss-linux-arm64-musl@1.32.0", + "bom-ref": "istanbul-reports@3.2.0", "type": "library", - "name": "lightningcss-linux-arm64-musl", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-linux-arm64-musl@1.32.0", + "name": "istanbul-reports", + "version": "3.2.0", + "scope": "required", + "author": "Krishnan Anantheswaran ", + "description": "istanbul reports", + "purl": "pkg:npm/istanbul-reports@3.2.0", "properties": [ { "name": "cdx:npm:package:development", @@ -2325,64 +1919,59 @@ ], "externalReferences": [ { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz" - } - ], - "hashes": [ + "type": "vcs", + "url": "git+ssh://git@github.com/istanbuljs/istanbuljs.git" + }, { - "alg": "SHA-512", - "content": "529424a1e9ebe14244ce054862923cd250c5bd198f560ea8a9ba0d1dfa07e024087cd03e1cead9ecca3b2993f4d9d0ba2e38213d025e06cbd7849a1dff09c806" + "type": "website", + "url": "https://istanbul.js.org/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/istanbuljs/istanbuljs/issues" } ], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "BSD-3-Clause" } } ] }, { - "bom-ref": "lightningcss-linux-x64-gnu@1.32.0", + "bom-ref": "js-tokens@10.0.0", "type": "library", - "name": "lightningcss-linux-x64-gnu", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-linux-x64-gnu@1.32.0", + "name": "js-tokens", + "version": "10.0.0", + "scope": "required", + "author": "Simon Lydell", + "description": "Tiny JavaScript tokenizer.", + "purl": "pkg:npm/js-tokens@10.0.0", "properties": [ { "name": "cdx:npm:package:development", "value": "true" } ], - "externalReferences": [ - { - "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz" - } - ], - "hashes": [ - { - "alg": "SHA-512", - "content": "57b42be7622166674a3d5afe56dc3ca3e58bb1025809377c96821fa4368c45619465f04e60425ec89224a862033193f03f1db8a5431fc12c7123ca61aff31b38" - } - ], + "externalReferences": [], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "MIT" } } ] }, { - "bom-ref": "lightningcss-linux-x64-musl@1.32.0", + "bom-ref": "json-schema-traverse@1.0.0", "type": "library", - "name": "lightningcss-linux-x64-musl", - "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-linux-x64-musl@1.32.0", + "name": "json-schema-traverse", + "version": "1.0.0", + "scope": "required", + "author": "Evgeny Poberezkin", + "description": "Traverse JSON Schema passing each schema object to callback", + "purl": "pkg:npm/json-schema-traverse@1.0.0", "properties": [ { "name": "cdx:npm:package:development", @@ -2392,30 +1981,43 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz" + "url": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/epoberezkin/json-schema-traverse.git" + }, + { + "type": "website", + "url": "https://github.com/epoberezkin/json-schema-traverse#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/epoberezkin/json-schema-traverse/issues" } ], "hashes": [ { "alg": "SHA-512", - "content": "6d870ba7e55bd1ac2c89783ff34b8245ecc2607360d7f9779add20cc79d657d5cfd56e6c29ae7f4c274659a47fcc13363de17f1dbb10bff8f651134e895bb15a" + "content": "34cf3f3fd9f75e35e12199f594b86415a0024ce5114178d6855e0103f4673aff31be0aadaa9017f483b89914314b1d51968e2dab37aa6f4b0e96bb9a3b2dddba" } ], "licenses": [ { "license": { - "id": "MPL-2.0" + "id": "MIT" } } ] }, { - "bom-ref": "lightningcss-win32-arm64-msvc@1.32.0", + "bom-ref": "lightningcss@1.32.0", "type": "library", - "name": "lightningcss-win32-arm64-msvc", + "name": "lightningcss", "version": "1.32.0", - "scope": "optional", - "purl": "pkg:npm/lightningcss-win32-arm64-msvc@1.32.0", + "scope": "required", + "description": "A CSS parser, transformer, and minifier written in Rust", + "purl": "pkg:npm/lightningcss@1.32.0", "properties": [ { "name": "cdx:npm:package:development", @@ -2425,13 +2027,17 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz" + "url": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/parcel-bundler/lightningcss.git" } ], "hashes": [ { "alg": "SHA-512", - "content": "f126c2f01478d294ba6da08cf2c6ed6034b011541de099454ce95a0f78161877d385370006734305d6ba79365ea9ba1f6a5209845c74a8ace01c999c3d39c677" + "content": "357601ce29cdadb95fada3c6cab6cfa03d7d0b587d95f23fd66ce0598bd75137b8d781b3fd7d450f65c165264ceeb453acc03c24bdceb4068689fac82a1439c9" } ], "licenses": [ @@ -2443,12 +2049,13 @@ ] }, { - "bom-ref": "lightningcss-win32-x64-msvc@1.32.0", + "bom-ref": "lightningcss-darwin-arm64@1.32.0", "type": "library", - "name": "lightningcss-win32-x64-msvc", + "name": "lightningcss-darwin-arm64", "version": "1.32.0", "scope": "optional", - "purl": "pkg:npm/lightningcss-win32-x64-msvc@1.32.0", + "description": "A CSS parser, transformer, and minifier written in Rust", + "purl": "pkg:npm/lightningcss-darwin-arm64@1.32.0", "properties": [ { "name": "cdx:npm:package:development", @@ -2458,13 +2065,17 @@ "externalReferences": [ { "type": "distribution", - "url": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz" + "url": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz" + }, + { + "type": "vcs", + "url": "https://github.com/parcel-bundler/lightningcss.git" } ], "hashes": [ { "alg": "SHA-512", - "content": "026abd07f4a86587438b5905ae88e7a2a3cbc58850e16a395e22fc11526b56c07c011a02d4f596e951ad4f458a09e9a3cbc682fa5a2e2678d2ed4d7cc776f4e9" + "content": "473786f49bb96da83606fd7f97095526f044deae93b57b247592cb0b27e0e69b7e1cbcfd06a94808eecb64ced51cd4d39ffe4f4611c50528e4e65738726b1c3d" } ], "licenses": [ @@ -2481,6 +2092,8 @@ "name": "magic-string", "version": "0.30.21", "scope": "required", + "author": "Rich Harris", + "description": "Modify strings, generate sourcemaps", "purl": "pkg:npm/magic-string@0.30.21", "properties": [ { @@ -2492,6 +2105,10 @@ { "type": "distribution", "url": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/Rich-Harris/magic-string.git" } ], "hashes": [ @@ -2514,6 +2131,7 @@ "name": "magicast", "version": "0.5.2", "scope": "required", + "description": "Modify a JS/TS file and write back magically just like JSON!", "purl": "pkg:npm/magicast@0.5.2", "properties": [ { @@ -2536,6 +2154,8 @@ "name": "make-dir", "version": "4.0.0", "scope": "required", + "author": "Sindre Sorhus", + "description": "Make a directory and its parents if needed - Think `mkdir -p`", "purl": "pkg:npm/make-dir@4.0.0", "properties": [ { @@ -2558,6 +2178,8 @@ "name": "nanoid", "version": "3.3.16", "scope": "required", + "author": "Andrey Sitnik ", + "description": "A tiny (116 bytes), secure URL-friendly unique string ID generator", "purl": "pkg:npm/nanoid@3.3.16", "properties": [ { @@ -2591,12 +2213,18 @@ "name": "neo4j-driver", "version": "6.2.0", "scope": "required", + "author": "Neo4j", + "description": "The official Neo4j driver for Javascript", "purl": "pkg:npm/neo4j-driver@6.2.0", "properties": [], "externalReferences": [ { "type": "distribution", "url": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-6.2.0.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/neo4j/neo4j-javascript-driver.git" } ], "hashes": [ @@ -2619,12 +2247,26 @@ "name": "neo4j-driver-bolt-connection", "version": "6.2.0", "scope": "required", + "author": "Neo4j", + "description": "Implements the connection with the Neo4j Database using the Bolt Protocol", "purl": "pkg:npm/neo4j-driver-bolt-connection@6.2.0", "properties": [], "externalReferences": [ { "type": "distribution", "url": "https://registry.npmjs.org/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-6.2.0.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/neo4j/neo4j-javascript-driver.git" + }, + { + "type": "website", + "url": "https://github.com/neo4j/neo4j-javascript-driver#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/neo4j/neo4j-javascript-driver/issues" } ], "hashes": [ @@ -2647,12 +2289,26 @@ "name": "neo4j-driver-core", "version": "6.2.0", "scope": "required", + "author": "Neo4j", + "description": "Internals of neo4j-driver", "purl": "pkg:npm/neo4j-driver-core@6.2.0", "properties": [], "externalReferences": [ { "type": "distribution", "url": "https://registry.npmjs.org/neo4j-driver-core/-/neo4j-driver-core-6.2.0.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/neo4j/neo4j-javascript-driver.git" + }, + { + "type": "website", + "url": "https://github.com/neo4j/neo4j-javascript-driver#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/neo4j/neo4j-javascript-driver/issues" } ], "hashes": [ @@ -2675,6 +2331,8 @@ "name": "obug", "version": "2.1.1", "scope": "required", + "author": "Kevin Deng ", + "description": "A lightweight JavaScript debugging utility, forked from debug, featuring TypeScript and ESM support.", "purl": "pkg:npm/obug@2.1.1", "properties": [ { @@ -2682,7 +2340,20 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git+https://github.com/sxzz/obug.git" + }, + { + "type": "website", + "url": "https://github.com/sxzz/obug#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/sxzz/obug/issues" + } + ], "licenses": [ { "license": { @@ -2697,6 +2368,7 @@ "name": "pathe", "version": "2.0.3", "scope": "required", + "description": "Universal filesystem path utils", "purl": "pkg:npm/pathe@2.0.3", "properties": [ { @@ -2730,6 +2402,8 @@ "name": "picocolors", "version": "1.1.1", "scope": "required", + "author": "Alexey Raspopov", + "description": "The tiniest and the fastest library for terminal output formatting with ANSI colors", "purl": "pkg:npm/picocolors@1.1.1", "properties": [ { @@ -2763,6 +2437,8 @@ "name": "picomatch", "version": "4.0.5", "scope": "required", + "author": "Jon Schlinkert (https://github.com/jonschlinkert)", + "description": "Blazing fast and accurate glob matcher written in JavaScript, with no dependencies and full support for standard and extended Bash glob features, including braces, extglobs, POSIX brackets, and regular expressions.", "purl": "pkg:npm/picomatch@4.0.5", "properties": [ { @@ -2774,6 +2450,14 @@ { "type": "distribution", "url": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz" + }, + { + "type": "website", + "url": "https://github.com/micromatch/picomatch" + }, + { + "type": "issue-tracker", + "url": "https://github.com/micromatch/picomatch/issues" } ], "hashes": [ @@ -2796,6 +2480,8 @@ "name": "postcss", "version": "8.5.17", "scope": "required", + "author": "Andrey Sitnik ", + "description": "Tool for transforming styles with JS plugins", "purl": "pkg:npm/postcss@8.5.17", "properties": [ { @@ -2807,6 +2493,14 @@ { "type": "distribution", "url": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz" + }, + { + "type": "website", + "url": "https://postcss.org/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/postcss/postcss/issues" } ], "hashes": [ @@ -2829,6 +2523,8 @@ "name": "require-from-string", "version": "2.0.2", "scope": "required", + "author": "Vsevolod Strukchinsky", + "description": "Require module from string", "purl": "pkg:npm/require-from-string@2.0.2", "properties": [ { @@ -2862,6 +2558,7 @@ "name": "rolldown", "version": "1.1.5", "scope": "required", + "description": "Fast JavaScript/TypeScript bundler in Rust with Rollup-compatible API.", "purl": "pkg:npm/rolldown@1.1.5", "properties": [ { @@ -2873,6 +2570,14 @@ { "type": "distribution", "url": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/rolldown/rolldown.git" + }, + { + "type": "website", + "url": "https://rolldown.rs/" } ], "hashes": [ @@ -2895,9 +2600,24 @@ "name": "rxjs", "version": "7.8.2", "scope": "required", + "author": "Ben Lesh ", + "description": "Reactive Extensions for modern JavaScript", "purl": "pkg:npm/rxjs@7.8.2", "properties": [], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/reactivex/rxjs.git" + }, + { + "type": "website", + "url": "https://rxjs.dev" + }, + { + "type": "issue-tracker", + "url": "https://github.com/ReactiveX/RxJS/issues" + } + ], "licenses": [ { "license": { @@ -2912,12 +2632,26 @@ "name": "safe-buffer", "version": "5.2.1", "scope": "required", + "author": "Feross Aboukhadijeh", + "description": "Safer Node.js Buffer API", "purl": "pkg:npm/safe-buffer@5.2.1", "properties": [], "externalReferences": [ { "type": "distribution", "url": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/feross/safe-buffer.git" + }, + { + "type": "website", + "url": "https://github.com/feross/safe-buffer" + }, + { + "type": "issue-tracker", + "url": "https://github.com/feross/safe-buffer/issues" } ], "hashes": [ @@ -2940,6 +2674,8 @@ "name": "semver", "version": "7.7.4", "scope": "required", + "author": "GitHub Inc.", + "description": "The semantic version parser used by npm.", "purl": "pkg:npm/semver@7.7.4", "properties": [ { @@ -2947,7 +2683,12 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git+https://github.com/npm/node-semver.git" + } + ], "licenses": [ { "license": { @@ -2962,6 +2703,8 @@ "name": "siginfo", "version": "2.0.0", "scope": "required", + "author": "Emil Bay ", + "description": "Utility module to print pretty messages on SIGINFO/SIGUSR1", "purl": "pkg:npm/siginfo@2.0.0", "properties": [ { @@ -2969,7 +2712,20 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git+https://github.com/emilbayes/siginfo.git" + }, + { + "type": "website", + "url": "https://github.com/emilbayes/siginfo#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/emilbayes/siginfo/issues" + } + ], "licenses": [ { "license": { @@ -2984,6 +2740,8 @@ "name": "source-map-js", "version": "1.2.1", "scope": "required", + "author": "Valentin 7rulnik Semirulnik ", + "description": "Generates and consumes source maps", "purl": "pkg:npm/source-map-js@1.2.1", "properties": [ { @@ -2991,7 +2749,12 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/7rulnik/source-map-js" + } + ], "licenses": [ { "license": { @@ -3006,6 +2769,8 @@ "name": "stackback", "version": "0.0.2", "scope": "required", + "author": "Roman Shtylman ", + "description": "return list of CallSite objects from a captured stacktrace", "purl": "pkg:npm/stackback@0.0.2", "properties": [ { @@ -3013,7 +2778,12 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git://github.com/shtylman/node-stackback.git" + } + ], "licenses": [ { "license": { @@ -3028,6 +2798,7 @@ "name": "std-env", "version": "4.1.0", "scope": "required", + "description": "Runtime agnostic JS utils", "purl": "pkg:npm/std-env@4.1.0", "properties": [ { @@ -3050,12 +2821,21 @@ "name": "string_decoder", "version": "1.3.0", "scope": "required", + "description": "The string_decoder module from Node core", "purl": "pkg:npm/string_decoder@1.3.0", "properties": [], "externalReferences": [ { "type": "distribution", "url": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + }, + { + "type": "vcs", + "url": "git://github.com/nodejs/string_decoder.git" + }, + { + "type": "website", + "url": "https://github.com/nodejs/string_decoder" } ], "hashes": [ @@ -3078,6 +2858,8 @@ "name": "supports-color", "version": "7.2.0", "scope": "required", + "author": "Sindre Sorhus", + "description": "Detect whether a terminal supports color", "purl": "pkg:npm/supports-color@7.2.0", "properties": [ { @@ -3122,6 +2904,8 @@ "name": "tinyexec", "version": "1.1.1", "scope": "required", + "author": "James Garbutt (https://github.com/43081j)", + "description": "A minimal library for executing processes in Node", "purl": "pkg:npm/tinyexec@1.1.1", "properties": [ { @@ -3129,7 +2913,20 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git+https://github.com/tinylibs/tinyexec.git" + }, + { + "type": "website", + "url": "https://github.com/tinylibs/tinyexec#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/tinylibs/tinyexec/issues" + } + ], "licenses": [ { "license": { @@ -3144,6 +2941,8 @@ "name": "tinyglobby", "version": "0.2.17", "scope": "required", + "author": "Superchupu", + "description": "A fast and minimal alternative to globby and fast-glob", "purl": "pkg:npm/tinyglobby@0.2.17", "properties": [ { @@ -3155,6 +2954,18 @@ { "type": "distribution", "url": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/SuperchupuDev/tinyglobby.git" + }, + { + "type": "website", + "url": "https://superchupu.dev/tinyglobby" + }, + { + "type": "issue-tracker", + "url": "https://github.com/SuperchupuDev/tinyglobby/issues" } ], "hashes": [ @@ -3177,6 +2988,7 @@ "name": "tinyrainbow", "version": "3.1.0", "scope": "required", + "description": "A small library to print colourful messages.", "purl": "pkg:npm/tinyrainbow@3.1.0", "properties": [ { @@ -3184,7 +2996,20 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "git+https://github.com/tinylibs/tinyrainbow.git" + }, + { + "type": "website", + "url": "https://github.com/tinylibs/tinyrainbow#readme" + }, + { + "type": "issue-tracker", + "url": "https://github.com/tinylibs/tinyrainbow/issues" + } + ], "licenses": [ { "license": { @@ -3199,9 +3024,24 @@ "name": "tslib", "version": "2.8.1", "scope": "required", + "author": "Microsoft Corp.", + "description": "Runtime library for TypeScript helper functions", "purl": "pkg:npm/tslib@2.8.1", "properties": [], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Microsoft/tslib.git" + }, + { + "type": "website", + "url": "https://www.typescriptlang.org/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/Microsoft/TypeScript/issues" + } + ], "licenses": [ { "license": { @@ -3216,9 +3056,24 @@ "name": "typescript", "version": "6.0.3", "scope": "required", + "author": "Microsoft Corp.", + "description": "TypeScript is a language for application scale JavaScript development", "purl": "pkg:npm/typescript@6.0.3", "properties": [], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/microsoft/TypeScript.git" + }, + { + "type": "website", + "url": "https://www.typescriptlang.org/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/microsoft/TypeScript/issues" + } + ], "licenses": [ { "license": { @@ -3233,6 +3088,7 @@ "name": "undici-types", "version": "8.3.0", "scope": "required", + "description": "A stand-alone types package for Undici", "purl": "pkg:npm/undici-types@8.3.0", "properties": [ { @@ -3244,6 +3100,18 @@ { "type": "distribution", "url": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/nodejs/undici.git" + }, + { + "type": "website", + "url": "https://undici.nodejs.org" + }, + { + "type": "issue-tracker", + "url": "https://github.com/nodejs/undici/issues" } ], "hashes": [ @@ -3266,6 +3134,8 @@ "name": "vite", "version": "8.1.4", "scope": "required", + "author": "Evan You", + "description": "Native-ESM powered web dev build tool", "purl": "pkg:npm/vite@8.1.4", "properties": [ { @@ -3277,6 +3147,18 @@ { "type": "distribution", "url": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitejs/vite.git" + }, + { + "type": "website", + "url": "https://vite.dev" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitejs/vite/issues" } ], "hashes": [ @@ -3299,6 +3181,8 @@ "name": "vitest", "version": "4.1.10", "scope": "required", + "author": "Anthony Fu ", + "description": "Next generation testing framework powered by Vite", "purl": "pkg:npm/vitest@4.1.10", "properties": [ { @@ -3310,6 +3194,18 @@ { "type": "distribution", "url": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/vitest-dev/vitest.git" + }, + { + "type": "website", + "url": "https://vitest.dev" + }, + { + "type": "issue-tracker", + "url": "https://github.com/vitest-dev/vitest/issues" } ], "hashes": [ @@ -3332,12 +3228,18 @@ "name": "web-tree-sitter", "version": "0.26.11", "scope": "required", + "author": "Max Brunsfeld", + "description": "Tree-sitter bindings for the web", "purl": "pkg:npm/web-tree-sitter@0.26.11", "properties": [], "externalReferences": [ { "type": "distribution", "url": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.11.tgz" + }, + { + "type": "vcs", + "url": "git+https://github.com/tree-sitter/tree-sitter.git" } ], "hashes": [ @@ -3360,6 +3262,8 @@ "name": "why-is-node-running", "version": "2.3.0", "scope": "required", + "author": "Mathias Buus (@mafintosh)", + "description": "Node is running but you don't know why? why-is-node-running is here to help you.", "purl": "pkg:npm/why-is-node-running@2.3.0", "properties": [ { @@ -3367,7 +3271,20 @@ "value": "true" } ], - "externalReferences": [], + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/mafintosh/why-is-node-running.git" + }, + { + "type": "website", + "url": "https://github.com/mafintosh/why-is-node-running" + }, + { + "type": "issue-tracker", + "url": "https://github.com/mafintosh/why-is-node-running/issues" + } + ], "licenses": [ { "license": { @@ -3379,7 +3296,7 @@ ], "dependencies": [ { - "ref": "@lubab/madar@0.29.0", + "ref": "@lubab/madar@0.30.0", "dependsOn": [ "@vscode/tree-sitter-wasm@0.3.1", "fflate@0.8.3", @@ -3420,25 +3337,6 @@ "ref": "@bcoe/v8-coverage@1.0.2", "dependsOn": [] }, - { - "ref": "@emnapi/core@1.11.1", - "dependsOn": [ - "@emnapi/wasi-threads@1.2.2", - "tslib@2.8.1" - ] - }, - { - "ref": "@emnapi/runtime@1.11.1", - "dependsOn": [ - "tslib@2.8.1" - ] - }, - { - "ref": "@emnapi/wasi-threads@1.2.2", - "dependsOn": [ - "tslib@2.8.1" - ] - }, { "ref": "@jridgewell/resolve-uri@3.1.2", "dependsOn": [] @@ -3454,80 +3352,14 @@ "@jridgewell/sourcemap-codec@1.5.5" ] }, - { - "ref": "@napi-rs/wasm-runtime@1.1.6", - "dependsOn": [ - "@tybys/wasm-util@0.10.3" - ] - }, { "ref": "@oxc-project/types@0.139.0", "dependsOn": [] }, - { - "ref": "@rolldown/binding-android-arm64@1.1.5", - "dependsOn": [] - }, { "ref": "@rolldown/binding-darwin-arm64@1.1.5", "dependsOn": [] }, - { - "ref": "@rolldown/binding-darwin-x64@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-freebsd-x64@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-arm-gnueabihf@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-arm64-gnu@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-arm64-musl@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-ppc64-gnu@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-s390x-gnu@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-x64-gnu@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-linux-x64-musl@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-openharmony-arm64@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-wasm32-wasi@1.1.5", - "dependsOn": [ - "@emnapi/core@1.11.1", - "@emnapi/runtime@1.11.1", - "@napi-rs/wasm-runtime@1.1.6" - ] - }, - { - "ref": "@rolldown/binding-win32-arm64-msvc@1.1.5", - "dependsOn": [] - }, - { - "ref": "@rolldown/binding-win32-x64-msvc@1.1.5", - "dependsOn": [] - }, { "ref": "@rolldown/pluginutils@1.0.1", "dependsOn": [] @@ -3536,12 +3368,6 @@ "ref": "@standard-schema/spec@1.1.0", "dependsOn": [] }, - { - "ref": "@tybys/wasm-util@0.10.3", - "dependsOn": [ - "tslib@2.8.1" - ] - }, { "ref": "@types/chai@5.2.3", "dependsOn": [ @@ -3567,7 +3393,6 @@ "ref": "@vitest/coverage-v8@4.1.10", "dependsOn": [ "@bcoe/v8-coverage@1.0.2", - "@vitest/utils@4.1.10", "ast-v8-to-istanbul@1.0.0", "istanbul-lib-coverage@3.2.2", "istanbul-lib-report@3.0.1", @@ -3575,7 +3400,8 @@ "magicast@0.5.2", "obug@2.1.1", "std-env@4.1.0", - "tinyrainbow@3.1.0" + "tinyrainbow@3.1.0", + "@vitest/utils@4.1.10" ] }, { @@ -3583,18 +3409,18 @@ "dependsOn": [ "@standard-schema/spec@1.1.0", "@types/chai@5.2.3", - "@vitest/spy@4.1.10", - "@vitest/utils@4.1.10", "chai@6.2.2", - "tinyrainbow@3.1.0" + "tinyrainbow@3.1.0", + "@vitest/spy@4.1.10", + "@vitest/utils@4.1.10" ] }, { "ref": "@vitest/mocker@4.1.10", "dependsOn": [ - "@vitest/spy@4.1.10", "estree-walker@3.0.3", - "magic-string@0.30.21" + "magic-string@0.30.21", + "@vitest/spy@4.1.10" ] }, { @@ -3606,17 +3432,17 @@ { "ref": "@vitest/runner@4.1.10", "dependsOn": [ - "@vitest/utils@4.1.10", - "pathe@2.0.3" + "pathe@2.0.3", + "@vitest/utils@4.1.10" ] }, { "ref": "@vitest/snapshot@4.1.10", "dependsOn": [ - "@vitest/pretty-format@4.1.10", - "@vitest/utils@4.1.10", "magic-string@0.30.21", - "pathe@2.0.3" + "pathe@2.0.3", + "@vitest/pretty-format@4.1.10", + "@vitest/utils@4.1.10" ] }, { @@ -3626,9 +3452,9 @@ { "ref": "@vitest/utils@4.1.10", "dependsOn": [ - "@vitest/pretty-format@4.1.10", "convert-source-map@2.0.0", - "tinyrainbow@3.1.0" + "tinyrainbow@3.1.0", + "@vitest/pretty-format@4.1.10" ] }, { @@ -3766,63 +3592,13 @@ "ref": "lightningcss@1.32.0", "dependsOn": [ "detect-libc@2.1.2", - "lightningcss-android-arm64@1.32.0", - "lightningcss-darwin-arm64@1.32.0", - "lightningcss-darwin-x64@1.32.0", - "lightningcss-freebsd-x64@1.32.0", - "lightningcss-linux-arm-gnueabihf@1.32.0", - "lightningcss-linux-arm64-gnu@1.32.0", - "lightningcss-linux-arm64-musl@1.32.0", - "lightningcss-linux-x64-gnu@1.32.0", - "lightningcss-linux-x64-musl@1.32.0", - "lightningcss-win32-arm64-msvc@1.32.0", - "lightningcss-win32-x64-msvc@1.32.0" + "lightningcss-darwin-arm64@1.32.0" ] }, - { - "ref": "lightningcss-android-arm64@1.32.0", - "dependsOn": [] - }, { "ref": "lightningcss-darwin-arm64@1.32.0", "dependsOn": [] }, - { - "ref": "lightningcss-darwin-x64@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-freebsd-x64@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-linux-arm-gnueabihf@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-linux-arm64-gnu@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-linux-arm64-musl@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-linux-x64-gnu@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-linux-x64-musl@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-win32-arm64-msvc@1.32.0", - "dependsOn": [] - }, - { - "ref": "lightningcss-win32-x64-msvc@1.32.0", - "dependsOn": [] - }, { "ref": "magic-string@0.30.21", "dependsOn": [ @@ -3900,21 +3676,7 @@ "dependsOn": [ "@oxc-project/types@0.139.0", "@rolldown/pluginutils@1.0.1", - "@rolldown/binding-android-arm64@1.1.5", - "@rolldown/binding-darwin-arm64@1.1.5", - "@rolldown/binding-darwin-x64@1.1.5", - "@rolldown/binding-freebsd-x64@1.1.5", - "@rolldown/binding-linux-arm-gnueabihf@1.1.5", - "@rolldown/binding-linux-arm64-gnu@1.1.5", - "@rolldown/binding-linux-arm64-musl@1.1.5", - "@rolldown/binding-linux-ppc64-gnu@1.1.5", - "@rolldown/binding-linux-s390x-gnu@1.1.5", - "@rolldown/binding-linux-x64-gnu@1.1.5", - "@rolldown/binding-linux-x64-musl@1.1.5", - "@rolldown/binding-openharmony-arm64@1.1.5", - "@rolldown/binding-wasm32-wasi@1.1.5", - "@rolldown/binding-win32-arm64-msvc@1.1.5", - "@rolldown/binding-win32-x64-msvc@1.1.5" + "@rolldown/binding-darwin-arm64@1.1.5" ] }, { @@ -4004,13 +3766,6 @@ { "ref": "vitest@4.1.10", "dependsOn": [ - "@vitest/expect@4.1.10", - "@vitest/mocker@4.1.10", - "@vitest/pretty-format@4.1.10", - "@vitest/runner@4.1.10", - "@vitest/snapshot@4.1.10", - "@vitest/spy@4.1.10", - "@vitest/utils@4.1.10", "es-module-lexer@2.1.0", "expect-type@1.3.0", "magic-string@0.30.21", @@ -4023,7 +3778,14 @@ "tinyglobby@0.2.17", "tinyrainbow@3.1.0", "vite@8.1.4", - "why-is-node-running@2.3.0" + "why-is-node-running@2.3.0", + "@vitest/expect@4.1.10", + "@vitest/mocker@4.1.10", + "@vitest/pretty-format@4.1.10", + "@vitest/runner@4.1.10", + "@vitest/spy@4.1.10", + "@vitest/snapshot@4.1.10", + "@vitest/utils@4.1.10" ] }, { diff --git a/src/cli/main.ts b/src/cli/main.ts index 3469e0b5..f22d331e 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -43,6 +43,7 @@ import { serveGraphStdio } from '../runtime/stdio-server.js' import { getNeighbors, getNode, loadGraph, queryGraph, shortestPath } from '../runtime/serve.js' import { formatTimeTravelResult } from '../runtime/time-travel.js' import { findPackageRoot, readPackageName, readPackageVersion } from '../shared/package-metadata.js' +import { resolveWorkspaceGraphPath } from '../shared/workspace.js' import { disableTelemetry, enableTelemetry, @@ -493,6 +494,7 @@ export function formatHelp(binaryName = 'madar'): string { ' --http explicit alias for HTTP transport', ' --stdio serve graph query methods over stdio (JSON lines)', ' --mcp alias for --stdio for installer/runtime parity', + ' --auto-refresh reconcile and watch the active workspace while serving over stdio', ' summary [graph.json] print a compact deterministic graph summary as JSON', ' try "" [path] one-command local first proof before agent install', ' query "" traverse graph.json for a question', @@ -764,6 +766,12 @@ function handleAgentCommand(command: AgentPlatform, args: string[], io: CliIO, d return 0 } +function warnWhenWorkspaceGraphIsMissing(io: CliIO): void { + if (!existsSync(resolveWorkspaceGraphPath())) { + io.log("Warning: out/graph.json not found. Run 'madar generate .' first, then re-run this command.") + } +} + export async function executeCli(argv: string[], io: CliIO = console, dependencies: CliDependencies = DEFAULT_DEPENDENCIES): Promise { const [command, ...args] = argv @@ -1113,16 +1121,18 @@ export async function executeCli(argv: string[], io: CliIO = console, dependenci if (command === 'serve') { const options = parseServeArgs(args) + const graphPath = resolveWorkspaceGraphPath(options.graphPath) if (options.transport === 'stdio') { await dependencies.serveGraphStdio({ - graphPath: options.graphPath, + graphPath, + ...(options.autoRefresh ? { autoRefresh: true, workspaceRoot: process.cwd() } : {}), logger: io, }) return 0 } await dependencies.serveGraph({ - graphPath: options.graphPath, + graphPath, host: options.host, port: options.port, logger: io, @@ -1263,8 +1273,8 @@ export async function executeCli(argv: string[], io: CliIO = console, dependenci if (command === 'claude') { const options = parsePlatformActionArgs(command, args) - if (options.action === 'install' && !existsSync('out/graph.json')) { - io.log("Warning: out/graph.json not found. Run 'madar generate .' first, then re-run this command.") + if (options.action === 'install') { + warnWhenWorkspaceGraphIsMissing(io) } if (options.action === 'install') { failureTelemetry = (failureBucket) => ({ @@ -1297,8 +1307,8 @@ export async function executeCli(argv: string[], io: CliIO = console, dependenci if (command === 'cursor') { const options = parsePlatformActionArgs(command, args) - if (options.action === 'install' && !existsSync('out/graph.json')) { - io.log("Warning: out/graph.json not found. Run 'madar generate .' first, then re-run this command.") + if (options.action === 'install') { + warnWhenWorkspaceGraphIsMissing(io) } if (options.action === 'install') { failureTelemetry = (failureBucket) => ({ @@ -1331,8 +1341,8 @@ export async function executeCli(argv: string[], io: CliIO = console, dependenci if (command === 'gemini') { const options = parsePlatformActionArgs(command, args) - if (options.action === 'install' && !existsSync('out/graph.json')) { - io.log("Warning: out/graph.json not found. Run 'madar generate .' first, then re-run this command.") + if (options.action === 'install') { + warnWhenWorkspaceGraphIsMissing(io) } if (options.action === 'install') { failureTelemetry = (failureBucket) => ({ @@ -1377,9 +1387,7 @@ export async function executeCli(argv: string[], io: CliIO = console, dependenci ...telemetryBase(dependencies), agentTarget: 'copilot', })) - if (!existsSync('out/graph.json')) { - io.log("Warning: out/graph.json not found. Run 'madar generate .' first, then re-run this command.") - } + warnWhenWorkspaceGraphIsMissing(io) io.log(dependencies.installSkill('copilot')) io.log(dependencies.installCopilotMcp('.', options.profile ? { profile: options.profile } : {})) emitTelemetry(io, dependencies, () => ({ diff --git a/src/cli/parser.ts b/src/cli/parser.ts index b1a73ffc..0d84d3a8 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -2,6 +2,7 @@ import { dirname, isAbsolute, resolve } from 'node:path' import type { ContextPackFormat, ContextPackRetrievalStrategy, ContextPackTaskKind } from '../contracts/context-pack.js' import { validateGraphOutputPath, validateGraphPath } from '../shared/security.js' +import { resolveWorkspaceGraphPath } from '../shared/workspace.js' import { type InstallPlatform, isInstallPlatform, type InstallProfile, isInstallProfile } from '../infrastructure/install.js' export class UsageError extends Error { @@ -198,6 +199,7 @@ export interface ServeCliOptions { host: string port: number transport: 'http' | 'stdio' + autoRefresh: boolean } export interface DoctorCliOptions { @@ -1503,11 +1505,18 @@ export function parseCompareArgs(args: string[]): CompareCliOptions { throw new UsageError('error: --exec is required') } - outputDir = validateGraphOutputPath(outputDir) + const resolvedGraphPath = resolveWorkspaceGraphPath(graphPath) + const graphArtifactDir = dirname(resolve(resolvedGraphPath)) + // Keep compare receipts beside the graph. This is especially important for + // linked worktrees, whose graph artifact directory intentionally lives + // outside the source checkout. + outputDir = outputDir === 'out/compare' + ? validateGraphOutputPath(resolve(graphArtifactDir, 'compare'), graphArtifactDir) + : validateGraphOutputPath(outputDir) return { question, - graphPath, + graphPath: resolvedGraphPath, execTemplate, questionsPath, outputDir, @@ -1608,10 +1617,16 @@ export function parseReviewCompareArgs(args: string[]): ReviewCompareCliOptions throw new UsageError('error: --exec is required') } + const resolvedGraphPath = resolveWorkspaceGraphPath(graphPath) + const graphArtifactDir = dirname(resolve(resolvedGraphPath)) + const resolvedOutputDir = outputDir === 'out/review-compare' + ? validateGraphOutputPath(resolve(graphArtifactDir, 'review-compare'), graphArtifactDir) + : validateReviewCompareOutputDir(outputDir) + return { - graphPath, + graphPath: resolvedGraphPath, execTemplate, - outputDir: validateReviewCompareOutputDir(outputDir), + outputDir: resolvedOutputDir, baseBranch, budget, yes, @@ -1970,6 +1985,7 @@ export function parseServeArgs(args: string[]): ServeCliOptions { let host = '127.0.0.1' let port = 4173 let transport: 'http' | 'stdio' = 'http' + let autoRefresh = false for (let index = 0; index < args.length; index += 1) { const argument = args[index] @@ -1979,7 +1995,7 @@ export function parseServeArgs(args: string[]): ServeCliOptions { if (!argument.startsWith('--')) { if (graphPath !== 'out/graph.json') { - throw new UsageError('Usage: madar serve [graph.json] [--host H] [--port N] [--transport http|stdio] [--http|--stdio|--mcp]') + throw new UsageError('Usage: madar serve [graph.json] [--host H] [--port N] [--transport http|stdio] [--http|--stdio|--mcp] [--auto-refresh]') } graphPath = argument continue @@ -1995,6 +2011,11 @@ export function parseServeArgs(args: string[]): ServeCliOptions { continue } + if (argument === '--auto-refresh') { + autoRefresh = true + continue + } + if (argument === '--transport') { transport = parseServeTransport('--transport', requireNonEmptyValue('--transport', args[index + 1])) index += 1 @@ -2034,7 +2055,7 @@ export function parseServeArgs(args: string[]): ServeCliOptions { throw new UsageError(`error: unknown option for serve: ${argument}`) } - return { graphPath, host, port, transport } + return { graphPath, host, port, transport, autoRefresh } } export function parseDoctorArgs(args: string[], commandName: 'doctor' | 'status' = 'doctor'): DoctorCliOptions { @@ -2169,11 +2190,12 @@ export function parseProofReportArgs(args: string[]): ProofReportCliOptions { graphPath = argument } - const graphBase = dirname(resolve(graphPath)) + const resolvedGraphPath = resolveWorkspaceGraphPath(graphPath) + const graphBase = dirname(resolve(resolvedGraphPath)) return { - graphPath, - outputDir: outputDir ?? resolve(graphBase, 'proof-report'), - compareDir: compareDir ?? resolve(graphBase, 'compare'), + graphPath: resolvedGraphPath, + outputDir: validateGraphOutputPath(outputDir ?? resolve(graphBase, 'proof-report'), graphBase), + compareDir: validateGraphOutputPath(compareDir ?? resolve(graphBase, 'compare'), graphBase), packPath, } } diff --git a/src/infrastructure/benchmark.ts b/src/infrastructure/benchmark.ts index 633c0520..bd5ef678 100644 --- a/src/infrastructure/benchmark.ts +++ b/src/infrastructure/benchmark.ts @@ -22,6 +22,7 @@ import { usageCaptureSummary, usageProviderLabel, } from './benchmark/usage.js' +import { resolveWorkspaceGraphPath } from '../shared/workspace.js' export { loadBenchmarkQuestions, querySubgraphTokens, type BenchmarkQuestionInput } from './benchmark/questions.js' @@ -282,9 +283,10 @@ export function runBenchmark( questions?: BenchmarkQuestionInput[], options: BenchmarkRunOptions = {}, ): BenchmarkResult | Promise { - const graph = loadBenchmarkGraph(graphPath) + const resolvedGraphPath = resolveWorkspaceGraphPath(graphPath) + const graph = loadBenchmarkGraph(resolvedGraphPath) const structureSignals = hasStructureSignalProvenance(graph) ? graphStructureMetrics(graph) : null - const baseline = resolveCorpusBaseline(graph.numberOfNodes(), { graphPath, corpusWords }) + const baseline = resolveCorpusBaseline(graph.numberOfNodes(), { graphPath: resolvedGraphPath, corpusWords }) const benchmarkQuestions = questions ?? SAMPLE_QUESTIONS const usesSampleQuestions = questions === undefined const evaluatedQuestions: BenchmarkQuestionResult[] = [] @@ -336,7 +338,7 @@ export function runBenchmark( ) } - return runRunnerBackedBenchmark(graph, graphPath, baseline, evaluatedQuestions, options) + return runRunnerBackedBenchmark(graph, resolvedGraphPath, baseline, evaluatedQuestions, options) .then((perQuestion) => finalizeBenchmarkResult( graph, diff --git a/src/infrastructure/benchmark/quality.ts b/src/infrastructure/benchmark/quality.ts index a6a814cc..63bef0b5 100644 --- a/src/infrastructure/benchmark/quality.ts +++ b/src/infrastructure/benchmark/quality.ts @@ -18,6 +18,7 @@ import { usageCaptureSummary, usageProviderLabel, } from './usage.js' +import { resolveWorkspaceGraphPath } from '../../shared/workspace.js' export interface GoldQuestion { question: string @@ -272,7 +273,7 @@ async function evaluateRunnerBackedQuestion( budget: number, options: QualityOptions & { execTemplate: string }, ): Promise { - const graphPath = options.graphPath ?? 'out/graph.json' + const graphPath = resolveWorkspaceGraphPath(options.graphPath ?? 'out/graph.json') const retrieval = qualityRetrieveContext(graph, gold.question, budget, graphPath) const run = await runBenchmarkPrompt({ graphPath, @@ -358,20 +359,23 @@ export function evaluateRetrievalQuality( budget = 3000, options: QualityOptions = {}, ): QualityReport | Promise { + const effectiveOptions = options.graphPath + ? { ...options, graphPath: resolveWorkspaceGraphPath(options.graphPath) } + : options const normalizedQuestions = questions.map((question) => normalizeGoldQuestion(question)) const skippedQuestions = normalizedQuestions.filter((question) => question === null).length const labeledQuestions = normalizedQuestions.filter((question): question is GoldQuestion => question !== null) if (!options.execTemplate) { - const results = labeledQuestions.map((question) => evaluateQuestion(graph, question, budget, options.graphPath)) - return buildQualityReport(graph, results, skippedQuestions, options) + const results = labeledQuestions.map((question) => evaluateQuestion(graph, question, budget, effectiveOptions.graphPath)) + return buildQualityReport(graph, results, skippedQuestions, effectiveOptions) } return (async () => { const results: QualityResult[] = [] for (const question of labeledQuestions) { - results.push(await evaluateRunnerBackedQuestion(graph, question, budget, options as QualityOptions & { execTemplate: string })) + results.push(await evaluateRunnerBackedQuestion(graph, question, budget, effectiveOptions as QualityOptions & { execTemplate: string })) } - return buildQualityReport(graph, results, skippedQuestions, options) + return buildQualityReport(graph, results, skippedQuestions, effectiveOptions) })() } diff --git a/src/infrastructure/benchmark/runner.ts b/src/infrastructure/benchmark/runner.ts index 8377354d..14dc60fa 100644 --- a/src/infrastructure/benchmark/runner.ts +++ b/src/infrastructure/benchmark/runner.ts @@ -1,12 +1,13 @@ import { spawn } from 'node:child_process' import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs' -import { basename, dirname, join, relative, resolve } from 'node:path' +import { dirname, join, relative, resolve } from 'node:path' import { KnowledgeGraph } from '../../contracts/graph.js' import type { ContextSessionDiagnostics, ContextSessionState } from '../../contracts/context-session.js' import { type RetrieveResult, retrieveContext } from '../../runtime/retrieve.js' import { QUERY_TOKEN_ESTIMATOR } from '../../runtime/serve.js' import { toShareSafeArtifactPath } from '../../shared/share-safe-artifacts.js' +import { readGraphSourceRoot } from '../../shared/graph-source-root.js' import { resolveShellCommand } from '../../shared/shell.js' import { validateGraphOutputPath } from '../../shared/security.js' import { buildMadarPromptPack, expandCompareExecTemplate } from '../compare.js' @@ -79,16 +80,7 @@ function portablePath(path: string): string { } function inferProjectRootFromGraphPath(graphPath: string): string { - let currentPath = dirname(resolve(graphPath)) - - while (dirname(currentPath) !== currentPath) { - if (basename(currentPath) === 'out') { - return dirname(currentPath) - } - currentPath = dirname(currentPath) - } - - return dirname(resolve(graphPath)) + return readGraphSourceRoot(graphPath) } function createBenchmarkOutputRoot(graphPath: string, outputDir: string | undefined, now: Date): string { diff --git a/src/infrastructure/cache.ts b/src/infrastructure/cache.ts index 20795b27..abc64921 100644 --- a/src/infrastructure/cache.ts +++ b/src/infrastructure/cache.ts @@ -4,6 +4,7 @@ import { join, resolve, basename } from 'node:path' import { binaryIngestSidecarPath } from '../shared/binary-ingest-sidecar.js' import { MAX_FETCH_BYTES } from '../shared/security.js' +import { resolveMadarOutputDirectory } from '../shared/workspace.js' function appendBinaryIngestSidecarHash(hash: ReturnType, filePath: string): void { const sidecarPath = binaryIngestSidecarPath(filePath) @@ -38,7 +39,7 @@ export function fileHash(filePath: string): string { } export function cacheDir(root: string = '.', ...segments: string[]): string { - const directory = join(root, 'out', 'cache', ...segments) + const directory = join(resolveMadarOutputDirectory(root), 'cache', ...segments) mkdirSync(directory, { recursive: true }) return directory } diff --git a/src/infrastructure/compare.ts b/src/infrastructure/compare.ts index 690684de..7ca37b8a 100644 --- a/src/infrastructure/compare.ts +++ b/src/infrastructure/compare.ts @@ -29,6 +29,7 @@ import { QUERY_TOKEN_ESTIMATOR, estimateQueryTokens, loadGraph } from '../runtim import { resolveToolProfileFromEnv, type McpToolProfile } from '../runtime/stdio/definitions.js' import { sidecarAwareFileFingerprint } from '../shared/binary-ingest-sidecar.js' import { sanitizeShareSafeText, toShareSafeArtifactPath, type ShareSafePathRoots } from '../shared/share-safe-artifacts.js' +import { readGraphSourceRoot } from '../shared/graph-source-root.js' import { resolveShellCommand, shellEscape } from '../shared/shell.js' import { MAX_TEXT_BYTES, validateGraphOutputPath, validateGraphPath } from '../shared/security.js' import { copyWorkspaceForBenchmark } from '../shared/workspace-copy.js' @@ -1991,16 +1992,7 @@ function portablePath(path: string): string { } function inferProjectRootFromGraphPath(graphPath: string): string { - let currentPath = dirname(resolve(graphPath)) - - while (dirname(currentPath) !== currentPath) { - if (basename(currentPath) === 'out') { - return dirname(currentPath) - } - currentPath = dirname(currentPath) - } - - return dirname(resolve(graphPath)) + return readGraphSourceRoot(graphPath) } export function resolveSuggestedGraphScopePath(graphPath: string, suggestedGraphScope: string): string { @@ -2909,7 +2901,7 @@ export function generateCompareArtifacts(input: GenerateCompareArtifactsInput): const retrieval = retrieveCompareContext(graph, question, retrievalBudget, projectRoot) const madarPrompt = buildMadarPromptPack({ - graphPath: input.graphPath, + graphPath, question, retrieval, ...(madarSession ? { session: madarSession } : {}), @@ -3692,7 +3684,6 @@ interface ImplementationReviewerVisibleGate { interface ImplementationWorkspace { tempRoot: string workspaceRoot: string - graphPath: string } function loadImplementationReviewerVisibleGates(questionsPath?: string | null): Map { @@ -3776,18 +3767,13 @@ function diffWorkspaceSnapshots(before: Map, after: Map typeof value === 'string') - const stdioIndex = normalizedArgs.indexOf('--stdio') - if (stdioIndex >= 0) { - const candidate = normalizedArgs[stdioIndex + 1] - if (candidate && candidate.trim().length > 0) { - return candidate - } - } - - const graphPathCandidate = normalizedArgs.find((value) => /out[\\/]+graph\.json$/i.test(value)) - return graphPathCandidate ?? null + return normalizedArgs.includes('serve') + && normalizedArgs.includes('--stdio') + && normalizedArgs.includes('--auto-refresh') } function readMcpCheck( label: McpCheck['label'], configPath: string, serversKey: 'mcpServers' | 'servers', - expectedGraphPath: string, ): McpCheck { if (!existsSync(configPath)) { return { @@ -284,23 +277,12 @@ function readMcpCheck( } } - const declaredGraphPath = extractGraphPathFromArgs(server.args) - if (!declaredGraphPath) { + if (!hasWorkspaceAutoRefreshArgs(server.args)) { return { label, configPath, status: 'stale', - reason: "graph path is missing from server args (expected '--stdio ')", - } - } - - const resolvedDeclaredGraphPath = resolve(declaredGraphPath) - if (resolvedDeclaredGraphPath !== expectedGraphPath) { - return { - label, - configPath, - status: 'stale', - reason: `points to ${resolvedDeclaredGraphPath}, expected ${expectedGraphPath}`, + reason: "server args must include 'serve --stdio --auto-refresh' to select the active workspace graph", } } @@ -368,7 +350,7 @@ function hasOpencodeMcpEntry(config: JsonObject | null): boolean { return config !== null && isRecord(config.mcp) && isRecord(config.mcp[OPENCODE_MCP_SERVER_NAME]) } -function isOpencodeMcpConfigured(config: JsonObject | null, expectedGraphPath: string): boolean { +function isOpencodeMcpConfigured(config: JsonObject | null): boolean { if (!config || !isRecord(config.mcp)) { return false } @@ -383,8 +365,7 @@ function isOpencodeMcpConfigured(config: JsonObject | null, expectedGraphPath: s return false } - const declaredGraphPath = extractGraphPathFromArgs(command) - return declaredGraphPath !== null && resolve(declaredGraphPath) === expectedGraphPath + return hasWorkspaceAutoRefreshArgs(command) } function computeNextCommands(report: Omit): string[] { @@ -445,16 +426,16 @@ function computeNextCommands(report: Omit { const loadGraphDependency = dependencies.loadGraph ?? loadGraph - const graph = loadGraphDependency(options.graphPath) + const graphPath = resolveWorkspaceGraphPath(options.graphPath) + const graph = loadGraphDependency(graphPath) const packOptions = { prompt: options.prompt, budget: options.budget, task: options.task, - graphPath: options.graphPath, + graphPath, format: 'json', verbose: true, ...(options.requireFreshGraph === true ? { requireFreshGraph: true } : {}), @@ -220,7 +222,7 @@ export async function runHandoffCommand( const schema = JSON.parse(contextPackPayload) as ContextPackSchemaV1 const artifact = buildHandoffArtifactV1(schema, { consumer: options.consumer, - artifactRoot: dirname(resolve(options.graphPath)), + artifactRoot: dirname(resolve(graphPath)), projectRoot: typeof graph.graph.root_path === 'string' && graph.graph.root_path.trim().length > 0 ? graph.graph.root_path : process.cwd(), diff --git a/src/infrastructure/install.ts b/src/infrastructure/install.ts index 9130c869..16361c93 100644 --- a/src/infrastructure/install.ts +++ b/src/infrastructure/install.ts @@ -128,15 +128,28 @@ const PLATFORM_CONFIG: Record = { // Cross-platform hook: pass the base64 payload as an argv argument so the // node -e command stays shell-neutral on macOS, Linux, and Windows. +const WORKSPACE_GRAPH_CHECK_MARKER = 'madar-workspace-graph-check' +const WORKSPACE_GRAPH_CHECK = [ + `/* ${WORKSPACE_GRAPH_CHECK_MARKER} */`, + `const fs=require('fs'),path=require('path');`, + `let directory=process.cwd(),hasGraph=false;`, + `for(;;){`, + `if(fs.existsSync(path.join(directory,'out','graph.json'))){hasGraph=true;break}`, + `try{if(fs.lstatSync(path.join(directory,'.git')).isFile()){hasGraph=true;break}}catch(e){}`, + `const parent=path.dirname(directory);`, + `if(parent===directory)break;`, + `directory=parent}`, +].join('') + function hookCommand(payloadJson: string): string { const b64 = Buffer.from(payloadJson).toString('base64') - return `node -e "try{require('fs').accessSync('out/graph.json');process.stdout.write(Buffer.from(process.argv[1],'base64').toString())}catch(e){}" "${b64}"` + return `node -e "${WORKSPACE_GRAPH_CHECK};if(hasGraph)process.stdout.write(Buffer.from(process.argv[1],'base64').toString())" "${b64}"` } function hookCommandWithFallback(matchJson: string, missJson: string): string { const b64Match = Buffer.from(matchJson).toString('base64') const b64Miss = Buffer.from(missJson).toString('base64') - return `node -e "var f;try{require('fs').accessSync('out/graph.json');f=process.argv[1]}catch(e){f=process.argv[2]}process.stdout.write(Buffer.from(f,'base64').toString())" "${b64Match}" "${b64Miss}"` + return `node -e "${WORKSPACE_GRAPH_CHECK};var f=hasGraph?process.argv[1]:process.argv[2];process.stdout.write(Buffer.from(f,'base64').toString())" "${b64Match}" "${b64Miss}"` } function decodeGeneratedHookPayloads(command: string): string[] { @@ -167,8 +180,9 @@ function decodeGeneratedHookPayloads(command: string): string[] { } function hookCommandHasGraphCheck(command: string): boolean { - return command.includes("accessSync('out/graph.json')") - || decodeGeneratedHookPayloads(command).some((payload) => payload.includes("accessSync('out/graph.json')")) + const hasGraphCheck = (value: string): boolean => + value.includes("accessSync('out/graph.json')") || value.includes(WORKSPACE_GRAPH_CHECK_MARKER) + return hasGraphCheck(command) || decodeGeneratedHookPayloads(command).some(hasGraphCheck) } function isMadarCodexHookPayload(payload: string): boolean { @@ -602,8 +616,32 @@ const OPENCODE_PLUGIN_REMINDER_COMMAND = `echo "[madar] Knowledge graph available. ${renderPlainMcpRoutingGuide()} ${strictNonMadarMcpRule(false).replace(/^for/, 'For')}. ${strictSkillOverrideRule(false)}. ${strictGraphReportFallbackRule(false).replace(/^do/, 'Do')}" && ` const OPENCODE_PLUGIN_JS = `// madar OpenCode plugin // Injects a knowledge graph reminder before bash tool calls when the graph exists. -import { existsSync } from "fs"; -import { join } from "path"; +import { existsSync, lstatSync } from "fs"; +import { dirname, join } from "path"; + +function hasMadarGraph(directory) { + let current = directory; + while (true) { + if (existsSync(join(current, "out", "graph.json"))) { + return true; + } + + // Linked Git worktrees store Madar artifacts outside the checkout. The + // installed MCP server builds that graph at session startup, so retain the + // reminder when this workspace is a linked worktree. + try { + if (lstatSync(join(current, ".git")).isFile()) { + return true; + } + } catch {} + + const parent = dirname(current); + if (parent === current) { + return false; + } + current = parent; + } +} export const MadarPlugin = async ({ directory }) => { let reminded = false; @@ -611,7 +649,7 @@ export const MadarPlugin = async ({ directory }) => { return { "tool.execute.before": async (input, output) => { if (reminded) return; - if (!existsSync(join(directory, "out", "graph.json"))) return; + if (!hasMadarGraph(directory)) return; if (input.tool === "bash") { output.args.command = @@ -699,8 +737,7 @@ function writeClaudePromptHookScript(projectDir: string, profile?: InstallProfil ) } -function codexPromptHookScript(projectDir: string): string { - const graphPath = resolve(projectDir, 'out', 'graph.json') +function codexPromptHookScript(): string { return `${CODEX_PROMPT_HOOK_SCRIPT_MARKER}\n${buildPromptApplicabilityHookScript( JSON.stringify({ hookSpecificOutput: { @@ -709,7 +746,6 @@ function codexPromptHookScript(projectDir: string): string { }, }), 'UserPromptSubmit', - graphPath, )}` } @@ -722,8 +758,7 @@ export function hasManagedCodexPromptHookScript(scriptPath: string): boolean { return false } - const projectDir = dirname(dirname(resolve(scriptPath))) - return readFileSync(scriptPath, 'utf8') === codexPromptHookScript(projectDir) + return readFileSync(scriptPath, 'utf8') === codexPromptHookScript() } function assertCodexPromptHookScriptIsSafe(projectDir: string): void { @@ -735,7 +770,7 @@ function assertCodexPromptHookScriptIsSafe(projectDir: string): void { function writeCodexPromptHookScript(projectDir: string): void { const hookScriptPath = join(projectDir, CODEX_PROMPT_HOOK_SCRIPT_RELATIVE_PATH) - const script = codexPromptHookScript(projectDir) + const script = codexPromptHookScript() assertCodexPromptHookScriptIsSafe(projectDir) if (existsSync(hookScriptPath) && readFileSync(hookScriptPath, 'utf8') === script) { return @@ -1680,9 +1715,11 @@ function installMcpServer( ensureParentDirectory(mcpJsonPath) const mcpConfig = readJsonObject(mcpJsonPath) - const graphPath = join(projectDir, 'out', 'graph.json') const isVscode = target === 'copilot' - const cliArgs = ['serve', '--stdio', graphPath] + // Resolve the graph from the MCP process's workspace at startup. A static + // install-time graph path would point every linked worktree back to the + // primary checkout. + const cliArgs = ['serve', '--stdio', '--auto-refresh'] // VS Code uses "servers" key, Claude/Cursor use "mcpServers" const serversKey = isVscode ? 'servers' : 'mcpServers' const mcpServers = ensureRecord(mcpConfig, serversKey) @@ -2091,13 +2128,13 @@ function hasUserManagedCodexMcpDeclaration(content: string): boolean { return false } -function renderCodexMcpBlock(graphPath: string, lineEnding: string, ownsPrecedingLineEnding = false): string { +function renderCodexMcpBlock(lineEnding: string, ownsPrecedingLineEnding = false): string { return [ CODEX_MCP_START_MARKER, ...(ownsPrecedingLineEnding ? [CODEX_MCP_OWNS_PRECEDING_LINE_ENDING_MARKER] : []), '[mcp_servers.madar]', 'command = "madar"', - `args = ["serve", "--stdio", ${JSON.stringify(graphPath)}]`, + 'args = ["serve", "--stdio", "--auto-refresh"]', 'env = { MADAR_TOOL_PROFILE = "core" }', 'enabled = true', CODEX_MCP_END_MARKER, @@ -2105,7 +2142,7 @@ function renderCodexMcpBlock(graphPath: string, lineEnding: string, ownsPrecedin ].join(lineEnding) } -export function isMadarCodexMcpConfig(content: string, expectedGraphPath: string): boolean { +export function isMadarCodexMcpConfig(content: string): boolean { try { const managedBlock = readManagedCodexMcpBlock(content) if (!managedBlock) { @@ -2118,7 +2155,7 @@ export function isMadarCodexMcpConfig(content: string, expectedGraphPath: string } const normalizedBlock = managedBlock.content.replaceAll('\r\n', '\n') - const expectedBlock = renderCodexMcpBlock(expectedGraphPath, '\n', managedBlock.ownsPrecedingLineEnding) + const expectedBlock = renderCodexMcpBlock('\n', managedBlock.ownsPrecedingLineEnding) return normalizedBlock === expectedBlock } catch { return false @@ -2134,13 +2171,12 @@ function assertCodexMcpConfigIsSafe(projectDir: string): void { function installCodexMcpServer(projectDir: string): string { const configPath = join(projectDir, CODEX_MCP_CONFIG_RELATIVE_PATH) - const graphPath = resolve(projectDir, 'out', 'graph.json') const content = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '' const managedBlock = readManagedCodexMcpBlock(content) const lineEnding = lineEndingForContent(content) const ownsPrecedingLineEnding = managedBlock?.ownsPrecedingLineEnding ?? (content.length > 0 && !content.endsWith('\n')) - const nextBlock = renderCodexMcpBlock(graphPath, lineEnding, ownsPrecedingLineEnding) + const nextBlock = renderCodexMcpBlock(lineEnding, ownsPrecedingLineEnding) const unownedContent = managedBlock ? `${content.slice(0, managedBlock.start)}${content.slice(managedBlock.end)}` : content @@ -2315,10 +2351,9 @@ function installOpencodeMcpServer(projectDir: string, packageRoot?: string): str const mcpWasRecord = isRecord(config.mcp) const mcp = ensureRecord(config, 'mcp') const existingServer = isRecord(mcp[OPENCODE_MCP_SERVER_NAME]) ? (mcp[OPENCODE_MCP_SERVER_NAME] as Record) : null - const graphPath = join(projectDir, 'out', 'graph.json') const serverConfig: Record = { type: 'local', - command: [process.execPath, resolvePackageCliPath(packageRoot), 'serve', '--stdio', graphPath], + command: [process.execPath, resolvePackageCliPath(packageRoot), 'serve', '--stdio', '--auto-refresh'], enabled: true, } diff --git a/src/infrastructure/proof-report.ts b/src/infrastructure/proof-report.ts index 517dd352..bb3f0a95 100644 --- a/src/infrastructure/proof-report.ts +++ b/src/infrastructure/proof-report.ts @@ -5,6 +5,7 @@ import { buildGraphSummary } from '../runtime/graph-summary.js' import { computeContextPackDiagnostics } from '../runtime/context-pack-diagnostics.js' import { loadGraph } from '../runtime/serve.js' import { validateGraphOutputPath } from '../shared/security.js' +import { resolveWorkspaceGraphPath } from '../shared/workspace.js' import type { CompiledContextPack, ContextPackCoverage, ContextPackNode, ContextPackTaskKind } from '../contracts/context-pack.js' import type { ContextPackDiagnostics } from '../contracts/context-pack-diagnostics.js' @@ -366,7 +367,8 @@ function uniqueOrdered(values: readonly string[]): string[] { } export function runProofReportCommand(options: ProofReportOptions): ProofReportResult { - const graphBase = relativeOutputBase(options.graphPath) + const graphPath = resolveWorkspaceGraphPath(options.graphPath) + const graphBase = relativeOutputBase(graphPath) const outputDir = validateGraphOutputPath(options.outputDir ?? join(graphBase, 'proof-report'), graphBase) const compareDir = options.compareDir ?? join(graphBase, 'compare') const compareSummaries = readCompareSummaries(compareDir, graphBase) @@ -374,20 +376,20 @@ export function runProofReportCommand(options: ProofReportOptions): ProofReportR const limitations: string[] = [] const nextCommands: string[] = [] const defaultCommands = [ - resolve(options.graphPath) === resolve('out/graph.json') ? 'madar summary out/graph.json' : `madar summary ${options.graphPath}`, - resolve(options.graphPath) === resolve('out/graph.json') ? 'madar doctor out/graph.json' : `madar doctor ${options.graphPath}`, + resolve(graphPath) === resolve('out/graph.json') ? 'madar summary out/graph.json' : `madar summary ${graphPath}`, + resolve(graphPath) === resolve('out/graph.json') ? 'madar doctor out/graph.json' : `madar doctor ${graphPath}`, ] const report = [ '# Local Proof Report', '', - ...formatGraphQualitySection(options.graphPath), + ...formatGraphQualitySection(graphPath), '', - ...formatWorkflowSection(options.graphPath), + ...formatWorkflowSection(graphPath), '', - ...formatPackSection(diagnostics, options.graphPath, nextCommands, limitations), + ...formatPackSection(diagnostics, graphPath, nextCommands, limitations), '', - ...formatCompareSection(compareSummaries, options.graphPath, nextCommands, limitations), + ...formatCompareSection(compareSummaries, graphPath, nextCommands, limitations), '', '## Limitations', '', diff --git a/src/infrastructure/review-compare.ts b/src/infrastructure/review-compare.ts index ff0211a2..14e37e01 100644 --- a/src/infrastructure/review-compare.ts +++ b/src/infrastructure/review-compare.ts @@ -1,7 +1,7 @@ import { createHash } from 'node:crypto' import { spawn } from 'node:child_process' import { mkdirSync, realpathSync, writeFileSync } from 'node:fs' -import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' import { analyzePrImpact, compactPrImpactResult } from '../runtime/pr-impact.js' import { estimateQueryTokens, loadGraph } from '../runtime/serve.js' @@ -11,6 +11,7 @@ import { type ShareSafePathRoots, } from '../shared/share-safe-artifacts.js' import { resolveShellCommand, shellEscape } from '../shared/shell.js' +import { readGraphSourceRoot } from '../shared/graph-source-root.js' import { findNearestExistingAncestor, validateGraphPath } from '../shared/security.js' import { buildContextPrompt } from './context-prompt.js' @@ -166,25 +167,17 @@ function rewriteShareSafeStderr( } function inferProjectRootFromGraphPath(graphPath: string): string { - let currentPath = dirname(resolve(graphPath)) - - while (dirname(currentPath) !== currentPath) { - if (basename(currentPath) === 'out') { - return dirname(currentPath) - } - currentPath = dirname(currentPath) - } - - return dirname(resolve(graphPath)) + return readGraphSourceRoot(graphPath) } function validateOutputDirForGraph(graphPath: string, outputDir: string): string { - const projectRoot = realpathSync(inferProjectRootFromGraphPath(graphPath)) - const baseDir = realpathSync(resolve(projectRoot, 'out')) + // Review artifacts live beside the graph. For a linked worktree that + // directory is intentionally outside the source checkout. + const baseDir = realpathSync(dirname(resolve(graphPath))) const resolvedOutputDir = isAbsolute(outputDir) ? resolve(outputDir) : outputDir === 'out' || outputDir.startsWith(`out${sep}`) - ? resolve(projectRoot, outputDir) + ? resolve(dirname(baseDir), outputDir) : resolve(outputDir) const existingAncestor = findNearestExistingAncestor(resolvedOutputDir) const normalizedOutputDir = existingAncestor === null diff --git a/src/infrastructure/time-travel.ts b/src/infrastructure/time-travel.ts index 6e11e0f5..d09782cb 100644 --- a/src/infrastructure/time-travel.ts +++ b/src/infrastructure/time-travel.ts @@ -1,5 +1,6 @@ import { execFileSync } from 'node:child_process' import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import type { KnowledgeGraph } from '../contracts/graph.js' @@ -7,7 +8,7 @@ import { EXTRACTOR_CACHE_VERSION } from '../pipeline/extract.js' import { loadGraph } from '../runtime/serve.js' import { compareTimeTravelGraphs, type CompareTimeTravelGraphsOptions, type TimeTravelResult } from '../runtime/time-travel.js' import { validateGraphOutputPath } from '../shared/security.js' -import { cacheDir } from './cache.js' +import { resolveMadarOutputDirectory, resolveMadarWorkspace } from '../shared/workspace.js' import { generateGraph, loadGraphExtractorVersion, type GenerateGraphOptions, type GenerateGraphResult } from './generate.js' type MaybePromise = T | Promise @@ -84,12 +85,12 @@ function defaultGitDependencies(rootDir: string): Required => { - const materializedWorktree = worktreePath(deps.rootDir, commitSha) + const materializedWorktree = worktreePath(commitSha) let worktreeCreated = false let buildError: unknown = null + let transientArtifactRoot: string | null = null try { await deps.git.createDetachedWorktree(materializedWorktree, commitSha) worktreeCreated = true + const transientWorkspace = resolveMadarWorkspace(materializedWorktree) + transientArtifactRoot = transientWorkspace.isLinkedWorktree ? transientWorkspace.artifactRoot : null const generated = await deps.generateGraph(materializedWorktree, { noHtml: true }) const extractorVersion = deps.loadGraphExtractorVersion(generated.graphPath) @@ -334,6 +343,16 @@ export async function loadOrBuildSnapshot(input: SnapshotRequest, dependencies: if (buildError == null) { throw cleanupError } + } finally { + if (transientArtifactRoot) { + try { + rmSync(transientArtifactRoot, { recursive: true, force: true }) + } catch { + // Snapshot publication succeeded; an orphaned scratch artifact is + // safe to leave behind and must not turn a completed comparison + // into a failure. + } + } } } } diff --git a/src/infrastructure/try-command.ts b/src/infrastructure/try-command.ts index 101e475f..e086cebb 100644 --- a/src/infrastructure/try-command.ts +++ b/src/infrastructure/try-command.ts @@ -9,6 +9,7 @@ import { buildGraphSummary, type GraphSummary } from '../runtime/graph-summary.j import { analyzeGraphContextFreshness, graphFreshnessStatusLabel, type GraphContextFreshness } from '../runtime/freshness.js' import { loadGraph } from '../runtime/serve.js' import { findPackageRoot } from '../shared/package-metadata.js' +import { resolveMadarWorkspace } from '../shared/workspace.js' interface TrialIo { log(message?: string): void @@ -60,7 +61,7 @@ const MIN_TRIAL_NODES = 10 const GETTING_STARTED_URL = 'https://github.com/mohanagy/madar/blob/main/docs/tutorials/getting-started.md' function trialGraphPath(workspace: string): string { - return join(workspace, 'out', 'graph.json') + return resolveMadarWorkspace(workspace).graphPath } function isReusableFreshnessStatus(status: GraphContextFreshness['status']): boolean { diff --git a/src/infrastructure/watch.ts b/src/infrastructure/watch.ts index c6e63d51..505b4791 100644 --- a/src/infrastructure/watch.ts +++ b/src/infrastructure/watch.ts @@ -1,19 +1,44 @@ -import { createHash } from 'node:crypto' -import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, watch as createFileSystemWatcher, writeFileSync } from 'node:fs' +import { createHash, randomUUID } from 'node:crypto' +import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, unlinkSync, watch as createFileSystemWatcher, writeFileSync } from 'node:fs' import { extname, join, resolve, sep } from 'node:path' import { AUDIO_EXTENSIONS, CODE_EXTENSIONS, DOC_EXTENSIONS, IMAGE_EXTENSIONS, OFFICE_EXTENSIONS, PAPER_EXTENSIONS, VIDEO_EXTENSIONS } from '../pipeline/detect.js' import { sidecarAwareFileFingerprint } from '../shared/binary-ingest-sidecar.js' import { collectGitVisibleFiles } from '../shared/git.js' -import { generateGraph } from './generate.js' +import { resolveMadarOutputDirectory } from '../shared/workspace.js' +import { generateGraph, type GenerateGraphResult } from './generate.js' export const WATCHED_EXTENSIONS = new Set([...CODE_EXTENSIONS, ...DOC_EXTENSIONS, ...PAPER_EXTENSIONS, ...IMAGE_EXTENSIONS, ...AUDIO_EXTENSIONS, ...VIDEO_EXTENSIONS, ...OFFICE_EXTENSIONS]) const MAX_SYMLINK_DEPTH = 40 const MAX_WATCHED_FILES = 10_000 const GIT_VISIBILITY_SNAPSHOT_KEY = '\0madar:git-visible-files' const GIT_VISIBILITY_CACHE_DURATION_MS = 500 +const REFRESH_LOCK_RETRY_MS = 50 +const REFRESH_LOCK_TIMEOUT_MS = 30_000 +const REFRESH_LOCK_STALE_MS = 60 * 60 * 1000 const WATCH_IGNORED_DIRECTORIES = new Set(['.git', 'out', 'node_modules', 'dist', 'build', 'target', 'venv', '.venv', 'env', '.env', '__pycache__']) +// These files can change the discovered corpus or the extraction environment +// even when no source file changes. Treat them as refresh triggers instead of +// waiting for an agent to remember a manual `madar generate --update`. +const WATCHED_CONTROL_FILENAMES = new Set([ + '.gitignore', + '.madarignore', + 'package.json', + 'tsconfig.json', + 'tsconfig.build.json', + 'jsconfig.json', + 'pyproject.toml', + 'go.mod', + 'go.sum', + 'Cargo.toml', + 'Cargo.lock', + 'pom.xml', + 'build.gradle', + 'build.gradle.kts', + 'settings.gradle', + 'settings.gradle.kts', +]) export interface WatchLogger { log(message?: string): void @@ -34,6 +59,15 @@ export interface WatchOptions extends RebuildCodeOptions { notifyOnly?: (watchPath: string, logger?: WatchLogger) => void } +export interface GraphAutoRefreshController { + /** Whether the initial incremental reconciliation produced a graph. */ + initialRebuilt: boolean + /** Stops the watcher and releases its filesystem resources. */ + stop(): void + /** Resolves once the watcher stops. */ + completed: Promise +} + interface WatchLoopSignal { wait(signal?: AbortSignal): Promise wake(): void @@ -122,7 +156,7 @@ function isWithinRoot(rootRealPath: string, candidateRealPath: string): boolean return candidateRealPath === rootRealPath || candidateRealPath.startsWith(rootPrefix) } -function gitignoreFingerprint(filePath: string, modifiedAt: number): string { +function controlFileFingerprint(filePath: string, modifiedAt: number): string { try { return createHash('sha256').update(readFileSync(filePath)).digest('hex') } catch { @@ -130,6 +164,106 @@ function gitignoreFingerprint(filePath: string, modifiedAt: number): string { } } +function sameFilesystemPath(left: string, right: string): boolean { + try { + return realpathSync(left) === realpathSync(right) + } catch { + return resolve(left) === resolve(right) + } +} + +function graphBelongsToWorkspace(graphPath: string, workspaceRoot: string): boolean { + try { + const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { root_path?: unknown } + return typeof parsed.root_path === 'string' + && parsed.root_path.trim().length > 0 + && sameFilesystemPath(parsed.root_path, workspaceRoot) + } catch { + return false + } +} + +function graphUsesSpi(graphPath: string): boolean { + try { + const parsed = JSON.parse(readFileSync(graphPath, 'utf8')) as { spi_mode?: unknown } + return parsed.spi_mode === true + } catch { + return false + } +} + +function pauseForRefreshLock(milliseconds: number): void { + // Rebuilds themselves are synchronous CPU/IO work. A short synchronous wait + // keeps the public `rebuildCode()` API synchronous while serializing two MCP + // servers that happen to attach to the same worktree. + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds) +} + +function isRefreshLeaseOwnerAlive(lockPath: string): boolean { + try { + const [pidValue] = readFileSync(lockPath, 'utf8').trim().split(/\s+/, 1) + const pid = Number(pidValue) + if (!Number.isSafeInteger(pid) || pid <= 0) { + return false + } + process.kill(pid, 0) + return true + } catch (error) { + const code = error && typeof error === 'object' && 'code' in error ? (error as { code?: unknown }).code : null + // EPERM means the process exists but belongs to another user. + return code === 'EPERM' + } +} + +function acquireRefreshLease(outputDir: string): () => void { + mkdirSync(outputDir, { recursive: true }) + const lockPath = join(outputDir, '.madar-refresh.lock') + const startedAt = Date.now() + + while (true) { + try { + const descriptor = openSync(lockPath, 'wx') + const leaseId = randomUUID() + try { + writeFileSync(descriptor, `${process.pid} ${leaseId} ${new Date().toISOString()}\n`, 'utf8') + } finally { + closeSync(descriptor) + } + return () => { + try { + const contents = readFileSync(lockPath, 'utf8') + if (contents.split(/\s+/, 3)[1] === leaseId) { + rmSync(lockPath, { force: true }) + } + } catch { + // The lease may have been recovered after a process crash. + } + } + } catch (error) { + const code = error && typeof error === 'object' && 'code' in error ? (error as { code?: unknown }).code : null + if (code !== 'EEXIST') { + throw error + } + } + + try { + if (Date.now() - statSync(lockPath).mtimeMs > REFRESH_LOCK_STALE_MS && !isRefreshLeaseOwnerAlive(lockPath)) { + rmSync(lockPath, { force: true }) + continue + } + } catch { + // Another process released the lease between the failed open and stat. + continue + } + + const remaining = REFRESH_LOCK_TIMEOUT_MS - (Date.now() - startedAt) + if (remaining <= 0) { + throw new Error(`Timed out waiting for another Madar refresh in ${outputDir}`) + } + pauseForRefreshLock(Math.min(REFRESH_LOCK_RETRY_MS, remaining)) + } +} + function collectWatchedFiles( directory: string, followSymlinks: boolean, @@ -137,7 +271,6 @@ function collectWatchedFiles( ancestorRealPaths: string[], snapshots: Map, includedFiles?: ReadonlySet, - watchGitignore = false, depth = 0, ): void { if (depth > MAX_SYMLINK_DEPTH || snapshots.size >= MAX_WATCHED_FILES) { @@ -152,8 +285,8 @@ function collectWatchedFiles( } for (const entry of entries) { - const isGitignore = watchGitignore && entry === '.gitignore' - if (entry.startsWith('.') && !isGitignore) { + const isControlFile = WATCHED_CONTROL_FILENAMES.has(entry) + if (entry.startsWith('.') && !isControlFile) { continue } if (WATCH_IGNORED_DIRECTORIES.has(entry)) { @@ -169,12 +302,12 @@ function collectWatchedFiles( } if (stats.isDirectory()) { - collectWatchedFiles(entryPath, followSymlinks, rootRealPath, ancestorRealPaths, snapshots, includedFiles, watchGitignore, depth + 1) + collectWatchedFiles(entryPath, followSymlinks, rootRealPath, ancestorRealPaths, snapshots, includedFiles, depth + 1) continue } - if (isGitignore && stats.isFile()) { - snapshots.set(entryPath, gitignoreFingerprint(entryPath, stats.mtimeMs)) + if (isControlFile && stats.isFile()) { + snapshots.set(entryPath, controlFileFingerprint(entryPath, stats.mtimeMs)) continue } @@ -202,7 +335,7 @@ function collectWatchedFiles( } if (targetStats.isDirectory()) { - collectWatchedFiles(entryPath, followSymlinks, rootRealPath, [...ancestorRealPaths, realTarget], snapshots, includedFiles, watchGitignore, depth + 1) + collectWatchedFiles(entryPath, followSymlinks, rootRealPath, [...ancestorRealPaths, realTarget], snapshots, includedFiles, depth + 1) continue } @@ -222,7 +355,7 @@ function collectWatchedFiles( } const extension = extname(entryPath).toLowerCase() - if (WATCHED_EXTENSIONS.has(extension) && (!includedFiles || includedFiles.has(entryPath))) { + if ((WATCHED_EXTENSIONS.has(extension) || isControlFile) && (!includedFiles || includedFiles.has(entryPath) || isControlFile)) { snapshots.set(entryPath, sidecarAwareFileFingerprint(entryPath, stats.mtimeMs)) } } @@ -259,7 +392,7 @@ function snapshotWatchedFiles( const visibleFiles = respectGitignore ? readGitVisibleFiles(resolvedWatchPath, gitVisibilityCache) : null const includedFiles = visibleFiles === null ? undefined : new Set(visibleFiles) - collectWatchedFiles(resolvedWatchPath, followSymlinks, rootRealPath, [rootRealPath], snapshots, includedFiles, visibleFiles !== null) + collectWatchedFiles(resolvedWatchPath, followSymlinks, rootRealPath, [rootRealPath], snapshots, includedFiles) if (visibleFiles !== null) { const visibilityFingerprint = createHash('sha256').update([...visibleFiles].sort().join('\0')).digest('hex') snapshots.set(GIT_VISIBILITY_SNAPSHOT_KEY, visibilityFingerprint) @@ -287,9 +420,10 @@ function diffSnapshots(previous: Map, next: Map = {}, +): GraphAutoRefreshController { + const controller = new AbortController() + const completed = watch(watchPath, debounceSeconds, { + ...options, + signal: controller.signal, + }) + const initialRebuilt = rebuildCode(watchPath, { + ...(options.followSymlinks !== undefined ? { followSymlinks: options.followSymlinks } : {}), + ...(options.respectGitignore !== undefined ? { respectGitignore: options.respectGitignore } : {}), + ...(options.noHtml !== undefined ? { noHtml: options.noHtml } : {}), + ...(options.logger !== undefined ? { logger: options.logger } : {}), + }) + + return { + initialRebuilt, + stop: () => controller.abort(), + completed, + } +} + export async function watch(watchPath: string, debounce = 3, options: WatchOptions = {}): Promise { const resolvedWatchPath = resolveWatchPath(watchPath) const output = defaultLogger(options.logger) diff --git a/src/pipeline/export.ts b/src/pipeline/export.ts index 28d4579b..46d53c6b 100644 --- a/src/pipeline/export.ts +++ b/src/pipeline/export.ts @@ -1,4 +1,4 @@ -import { mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join, relative } from 'node:path' import { KnowledgeGraph } from '../contracts/graph.js' @@ -378,6 +378,20 @@ function enrichPayloadWithBridgeMetadata(payload: HtmlPayload, bridgeMetadata: R } } +function writeFileAtomically(outputPath: string, content: string): void { + const temporaryPath = join( + dirname(outputPath), + `.madar-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`, + ) + + try { + writeFileSync(temporaryPath, content, 'utf8') + renameSync(temporaryPath, outputPath) + } finally { + rmSync(temporaryPath, { force: true }) + } +} + export function toJson( graph: KnowledgeGraph, communities: Communities, @@ -411,7 +425,10 @@ export function toJson( semantic_anomalies: semanticAnomalies, } - writeFileSync(outputPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8') + // MCP readers can reload this file while a watcher rebuilds. Publishing the + // completed JSON with a same-directory rename prevents a reader from ever + // observing a truncated graph. + writeFileAtomically(outputPath, `${JSON.stringify(data, null, 2)}\n`) } function subgraphFromNodes(graph: KnowledgeGraph, nodeIds: string[]): KnowledgeGraph { diff --git a/src/pipeline/federate.ts b/src/pipeline/federate.ts index 2461a1aa..88da7588 100644 --- a/src/pipeline/federate.ts +++ b/src/pipeline/federate.ts @@ -1,5 +1,5 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' -import { basename, dirname, join, resolve } from 'node:path' +import { basename, join, resolve } from 'node:path' import { KnowledgeGraph } from '../contracts/graph.js' import { buildFromJson } from './build.js' @@ -8,6 +8,7 @@ import { buildCommunityLabels } from './community-naming.js' import { generate as generateReport } from './report.js' import { toJson } from './export.js' import { isRecord } from '../shared/guards.js' +import { readGraphSourceRoot } from '../shared/graph-source-root.js' import { validateGraphPath } from '../shared/security.js' import { godNodes, semanticAnomalies, suggestQuestions, surprisingConnections } from './analyze.js' @@ -35,7 +36,7 @@ interface GraphSource { graph: KnowledgeGraph } -function loadSourceGraph(graphPath: string): KnowledgeGraph { +function loadSourceGraph(graphPath: string): { graph: KnowledgeGraph; graphPath: string } { const safePath = validateGraphPath(graphPath) if (readFileSync(safePath).byteLength > MAX_GRAPH_BYTES) { throw new Error(`Graph file too large: ${safePath}`) @@ -46,20 +47,20 @@ function loadSourceGraph(graphPath: string): KnowledgeGraph { throw new Error(`Invalid graph file: ${safePath}`) } - return buildFromJson({ + return { + graphPath: safePath, + graph: buildFromJson({ schema_version: parsed.schema_version, directed: parsed.directed === true, nodes: Array.isArray(parsed.nodes) ? parsed.nodes : [], edges: Array.isArray(parsed.links) ? parsed.links : Array.isArray(parsed.edges) ? parsed.edges : [], hyperedges: Array.isArray(parsed.hyperedges) ? parsed.hyperedges : [], - }, { directed: false, validateExtraction: false }) + }, { directed: false, validateExtraction: false }), + } } function inferRepoName(graphPath: string): string { - // out/graph.json -> parent of out - const madarOutDir = dirname(resolve(graphPath)) - const parentDir = dirname(madarOutDir) - return basename(parentDir) + return basename(readGraphSourceRoot(graphPath)) } function prefixNodeId(repoName: string, nodeId: string): string { @@ -139,12 +140,12 @@ export function federate(graphPaths: string[], options: FederateOptions = {}): F // Load all graphs and merge into federated graph for (const graphPath of graphPaths) { - const graph = loadSourceGraph(graphPath) - const repoName = inferRepoName(graphPath) - sources.push({ repoName, graphPath, graph }) + const source = loadSourceGraph(graphPath) + const repoName = inferRepoName(source.graphPath) + sources.push({ repoName, graphPath: source.graphPath, graph: source.graph }) // Add all nodes with repo prefix - for (const [nodeId, attributes] of graph.nodeEntries()) { + for (const [nodeId, attributes] of source.graph.nodeEntries()) { const prefixedId = prefixNodeId(repoName, nodeId) federatedGraph.addNode(prefixedId, { ...attributes, @@ -154,8 +155,8 @@ export function federate(graphPaths: string[], options: FederateOptions = {}): F } // Add all edges with repo prefix - for (const [source, target, attributes] of graph.edgeEntries()) { - const prefixedSource = prefixNodeId(repoName, source) + for (const [sourceNode, target, attributes] of source.graph.edgeEntries()) { + const prefixedSource = prefixNodeId(repoName, sourceNode) const prefixedTarget = prefixNodeId(repoName, target) federatedGraph.addEdge(prefixedSource, prefixedTarget, { ...attributes, diff --git a/src/pipeline/spi/cache.ts b/src/pipeline/spi/cache.ts index a9c0f37a..dac0dec9 100644 --- a/src/pipeline/spi/cache.ts +++ b/src/pipeline/spi/cache.ts @@ -19,7 +19,7 @@ // Cache layout // ──────────── // -// /out/.spi-cache/ +// /.spi-cache/ // index.json — cache metadata: { version, key, generated_at, file_count } // spi.json — serialized SemanticProgramIndex // @@ -52,11 +52,11 @@ import { } from 'node:fs' import { dirname, extname, join, relative, resolve } from 'node:path' +import { resolveMadarOutputDirectory } from '../../shared/workspace.js' import { buildSpi, type BuildSpiOptions, findNearestProjectConfigPath } from './build.js' import type { SemanticProgramIndex } from './types.js' const CACHE_DIR_NAME = '.spi-cache' -const CACHE_DIR_PARENT = 'out' const CACHE_INDEX_FILE = 'index.json' const CACHE_SPI_FILE = 'spi.json' const CACHE_FORMAT_VERSION = 1 @@ -101,7 +101,8 @@ export interface BuildSpiCachedOptions extends BuildSpiOptions { /** Disable the cache for this build (default: false). When true, the * call behaves exactly like buildSpi() — no read, no write. */ noCache?: boolean - /** Override the cache directory (default: `/out/.spi-cache`). + /** Override the cache directory (default: Madar's workspace-specific + * artifact output directory plus `.spi-cache`). * Useful for tests and for projects that want to relocate the cache * outside the default out tree. */ cacheDir?: string @@ -127,7 +128,7 @@ export interface BuildSpiCachedResult { export function buildSpiCached(opts: BuildSpiCachedOptions): BuildSpiCachedResult { const start = Date.now() const root = resolve(opts.root) - const cacheDir = opts.cacheDir ?? join(root, CACHE_DIR_PARENT, CACHE_DIR_NAME) + const cacheDir = opts.cacheDir ?? join(resolveMadarOutputDirectory(root), CACHE_DIR_NAME) const indexPath = join(cacheDir, CACHE_INDEX_FILE) const spiPath = join(cacheDir, CACHE_SPI_FILE) @@ -205,7 +206,7 @@ export function buildSpiCached(opts: BuildSpiCachedOptions): BuildSpiCachedResul /** Explicit cache invalidation — removes the on-disk artifacts. * Returns true iff anything was deleted. */ export function clearSpiCache(root: string, cacheDir?: string): boolean { - const dir = cacheDir ?? join(resolve(root), CACHE_DIR_PARENT, CACHE_DIR_NAME) + const dir = cacheDir ?? join(resolveMadarOutputDirectory(root), CACHE_DIR_NAME) if (!existsSync(dir)) return false rmSync(dir, { recursive: true, force: true }) return true diff --git a/src/runtime/mcp-response-evidence.ts b/src/runtime/mcp-response-evidence.ts index ed0a2847..0666eb05 100644 --- a/src/runtime/mcp-response-evidence.ts +++ b/src/runtime/mcp-response-evidence.ts @@ -4,6 +4,7 @@ import type { ContextPackExecutionSlice, ContextPackRuntimeGenerationAnswerContract, } from '../contracts/context-pack.js' +import { readGraphSourceRoot } from '../shared/graph-source-root.js' export type MadarResponsePackConfidence = 'high' | 'medium' | 'low' export type MadarResponseCoverage = 'complete' | 'partial' | 'unknown' @@ -159,7 +160,10 @@ function scopeQualityAssessment( } const expectedGraphPath = `${candidateScopes[0]}/out/graph.json` - if (normalizedGraphPath === expectedGraphPath || normalizedGraphPath.endsWith(`/${expectedGraphPath}`)) { + const normalizedSourceRoot = normalizeSourcePath(readGraphSourceRoot(graphPath)) + const sourceRootMatchesScope = normalizedSourceRoot === candidateScopes[0] + || normalizedSourceRoot.endsWith(`/${candidateScopes[0]}`) + if (normalizedGraphPath === expectedGraphPath || normalizedGraphPath.endsWith(`/${expectedGraphPath}`) || sourceRootMatchesScope) { return { confidenceCap: 'high', reason: `scope quality: graph scope is aligned with the ${candidateScopes[0]} runtime evidence`, diff --git a/src/runtime/stdio-server.ts b/src/runtime/stdio-server.ts index 0bbfd3c4..12fd5e82 100644 --- a/src/runtime/stdio-server.ts +++ b/src/runtime/stdio-server.ts @@ -1,10 +1,11 @@ import { createInterface } from 'node:readline' -import { statSync } from 'node:fs' -import { basename, dirname } from 'node:path' +import { existsSync, realpathSync, statSync } from 'node:fs' +import { basename, dirname, resolve } from 'node:path' import type { Readable, Writable } from 'node:stream' import type { ContextSessionState } from '../contracts/context-session.js' import { compareRefs } from '../infrastructure/time-travel.js' +import { startGraphAutoRefresh } from '../infrastructure/watch.js' import { diffGraphs } from './diff.js' import { buildGraphSummary } from './graph-summary.js' import { MCP_PROMPTS, MCP_TOOLS, activeMcpTools, isCoreToolName, resolveToolProfileFromEnv, type McpPromptDefinition } from './stdio/definitions.js' @@ -39,6 +40,8 @@ import { type TelemetryFailureBucket, } from '../shared/telemetry.js' import { findPackageRoot, readPackageVersion } from '../shared/package-metadata.js' +import { resolveGraphSourceRoot } from '../shared/graph-source-root.js' +import { resolveMadarWorkspace } from '../shared/workspace.js' const JSONRPC_PARSE_ERROR = -32700 const JSONRPC_INVALID_REQUEST = -32600 @@ -133,6 +136,12 @@ interface StdioResponse { export interface ServeGraphStdioOptions { graphPath: string + /** Reconcile once and watch the selected workspace for this MCP process. */ + autoRefresh?: boolean + /** Source root selected when the MCP process was launched. */ + workspaceRoot?: string + /** Internal/testing override for the filesystem change debounce. */ + autoRefreshDebounceSeconds?: number input?: Readable output?: Writable errorOutput?: Writable @@ -142,6 +151,24 @@ export interface ServeGraphStdioOptions { } } +function sameFilesystemPath(left: string, right: string): boolean { + try { + return realpathSync(left) === realpathSync(right) + } catch { + return resolve(left) === resolve(right) + } +} + +function graphRootPath(graphPath: string): string | null { + try { + const graph = loadGraph(validateGraphPath(graphPath)) + const rootPath = graph.graph.root_path + return typeof rootPath === 'string' && rootPath.trim().length > 0 ? rootPath.trim() : null + } catch { + return null + } +} + function ok(id: string | number | null, result: unknown): StdioResponse { return { jsonrpc: '2.0', id, result } } @@ -639,7 +666,7 @@ export function handleStdioRequest( // Only advertise semantic/rerank params when the optional transformers // package is actually resolvable on this machine — agents cannot pass // parameters that are absent from the schema. - const semanticAvailable = isSemanticRuntimeAvailable(dirname(graphPath)) + const semanticAvailable = isSemanticRuntimeAvailable(graphRootPath(graphPath) ?? resolveGraphSourceRoot(graphPath)) return ok(id, { tools: activeMcpTools(profile, { semanticAvailable }) }) } case 'tools/call': { @@ -666,7 +693,7 @@ export function handleStdioRequest( handleGraphDiff, compareRefs: async (input) => { const safeGraphPath = validateGraphPath(graphPath) - const projectRoot = dirname(dirname(safeGraphPath)) + const projectRoot = resolveGraphSourceRoot(safeGraphPath, loadGraphCached(safeGraphPath)) return await (toolOverrides.compareRefs ?? compareRefs)(input, { rootDir: projectRoot }) }, getContextPromptSession: (sessionId) => ensureContextPromptSessions(sessionState).get(sessionId), @@ -824,48 +851,83 @@ export async function serveGraphStdio(options: ServeGraphStdioOptions): Promise< const output = options.output ?? process.stdout const errorOutput = options.errorOutput ?? process.stderr const sessionState = createSessionState() + let autoRefresh: ReturnType | null = null + + if (options.autoRefresh) { + const workspaceRoot = options.workspaceRoot ?? graphRootPath(options.graphPath) + if (!workspaceRoot) { + throw new Error('Cannot auto-refresh a graph without a workspace root. Run madar generate from the workspace first.') + } + + const workspace = resolveMadarWorkspace(workspaceRoot) + if (!sameFilesystemPath(options.graphPath, workspace.graphPath)) { + throw new Error( + `Refusing to auto-refresh ${options.graphPath}: it is not the graph artifact for ${workspace.rootPath}. ` + + 'Start the MCP server from the intended worktree instead.', + ) + } + + autoRefresh = startGraphAutoRefresh(workspace.rootPath, options.autoRefreshDebounceSeconds ?? 1, { + // The MCP server needs graph.json; avoid regenerating the browser view on + // every coalesced agent edit. + noHtml: true, + logger: { log() {}, error() {} }, + }) + if (!autoRefresh.initialRebuilt && !existsSync(options.graphPath)) { + autoRefresh.stop() + await autoRefresh.completed + throw new Error(`Unable to build a graph for ${workspace.rootPath}`) + } + } errorOutput.write(`[madar serve] stdio ready for ${options.graphPath}\n`) const readline = createInterface({ input, crlfDelay: Infinity }) - for await (const line of readline) { - const trimmed = line.trim() - if (!trimmed) { - continue - } + try { + for await (const line of readline) { + const trimmed = line.trim() + if (!trimmed) { + continue + } - if (trimmed.length > MAX_STDIO_LINE_BYTES) { - const response = failure(null, JSONRPC_INVALID_REQUEST, `Payload too large (max ${MAX_STDIO_LINE_BYTES} bytes)`) - output.write(`${JSON.stringify(response)}\n`) - continue - } + if (trimmed.length > MAX_STDIO_LINE_BYTES) { + const response = failure(null, JSONRPC_INVALID_REQUEST, `Payload too large (max ${MAX_STDIO_LINE_BYTES} bytes)`) + output.write(`${JSON.stringify(response)}\n`) + continue + } - let payload: unknown - try { - payload = JSON.parse(trimmed) - } catch { - const response = failure(null, JSONRPC_PARSE_ERROR, 'Parse error') - emitLogNotification(output, sessionState, 'error', { message: response.error?.message ?? 'Parse error', code: JSONRPC_PARSE_ERROR }) - output.write(`${JSON.stringify(response)}\n`) - continue - } + let payload: unknown + try { + payload = JSON.parse(trimmed) + } catch { + const response = failure(null, JSONRPC_PARSE_ERROR, 'Parse error') + emitLogNotification(output, sessionState, 'error', { message: response.error?.message ?? 'Parse error', code: JSONRPC_PARSE_ERROR }) + output.write(`${JSON.stringify(response)}\n`) + continue + } - let response: StdioResponse | null - try { - emitResourceNotifications(output, options.graphPath, sessionState) - response = await Promise.resolve(handleStdioRequest(options.graphPath, payload, sessionState)) - } catch (error) { - // A rejected handler must never tear down the whole stdio server: every - // request gets an answer and the loop keeps serving (#crash). - const message = error instanceof Error ? error.message : 'Request failed' - response = failure(requestId(payload as StdioRequest), JSONRPC_SERVER_ERROR, message) - } - if (response) { - if (response.error) { - emitLogNotification(output, sessionState, 'error', { message: response.error.message, code: response.error.code }) + let response: StdioResponse | null + try { + emitResourceNotifications(output, options.graphPath, sessionState) + response = await Promise.resolve(handleStdioRequest(options.graphPath, payload, sessionState)) + } catch (error) { + // A rejected handler must never tear down the whole stdio server: every + // request gets an answer and the loop keeps serving (#crash). + const message = error instanceof Error ? error.message : 'Request failed' + response = failure(requestId(payload as StdioRequest), JSONRPC_SERVER_ERROR, message) + } + if (response) { + if (response.error) { + emitLogNotification(output, sessionState, 'error', { message: response.error.message, code: response.error.code }) + } + output.write(`${JSON.stringify(response)}\n`) } - output.write(`${JSON.stringify(response)}\n`) + } + } finally { + if (autoRefresh) { + autoRefresh.stop() + await autoRefresh.completed } } } diff --git a/src/runtime/stdio/tools.ts b/src/runtime/stdio/tools.ts index 110ab627..ffe05b13 100644 --- a/src/runtime/stdio/tools.ts +++ b/src/runtime/stdio/tools.ts @@ -22,6 +22,7 @@ import type { ContextSessionState } from '../../contracts/context-session.js' import { buildCommunityLabels } from '../../pipeline/community-naming.js' import { communityDetailsAtZoom, communityDetailsMicro, type CommunityZoomLevel } from '../../pipeline/community-details.js' import { lineNumberFromSourceLocation, lineRangeFromSourceLocation } from '../../shared/source-location.js' +import { resolveGraphSourceRoot } from '../../shared/graph-source-root.js' import { validateGraphPath } from '../../shared/security.js' import { featureMap } from '../feature-map.js' import { implementationChecklist } from '../implementation-checklist.js' @@ -772,14 +773,14 @@ function createImpactCandidate( } } -function snippetSourcePathCandidates(graphPath: string, sourceFile: string): string[] { +function snippetSourcePathCandidates(graphPath: string, sourceFile: string, projectRoot?: string): string[] { if (sourceFile.trim().length === 0) { return [] } const graphDir = dirname(graphPath) - const projectDir = basename(graphDir) === 'out' ? dirname(graphDir) : graphDir - const roots = [...new Set([graphDir, projectDir].map((root) => resolve(root)))] + const legacyProjectDir = basename(graphDir) === 'out' ? dirname(graphDir) : graphDir + const roots = [...new Set([graphDir, projectRoot ?? legacyProjectDir].map((root) => resolve(root)))] const candidates = isAbsolute(sourceFile) ? [resolve(sourceFile)] : roots.map((root) => resolve(root, sourceFile)) @@ -810,9 +811,9 @@ function readFocusedSnippet( graphPath: string, sourceFile: string, lineNumber: number, - options: { derived?: boolean; fileCache?: Map } = {}, + options: { derived?: boolean; fileCache?: Map; projectRoot?: string } = {}, ): string | null { - for (const candidatePath of snippetSourcePathCandidates(graphPath, sourceFile)) { + for (const candidatePath of snippetSourcePathCandidates(graphPath, sourceFile, options.projectRoot)) { const snippet = readSnippet(candidatePath, lineNumber, options) if (snippet !== null) { return snippet @@ -952,6 +953,7 @@ function buildFocusedExpansionPayload( const communityIds = new Set() const includedIds = new Set() const snippetFileCache = new Map() + const projectRoot = resolveGraphSourceRoot(graphPath, graph) for (const [nodeId, attributes] of graph.nodeEntries()) { const sourceFile = String(attributes.source_file ?? '').trim() @@ -1004,6 +1006,7 @@ function buildFocusedExpansionPayload( const snippet = readFocusedSnippet(graphPath, sourceFile, lineNumber, { derived: derived || sourceRange === null, fileCache: snippetFileCache, + projectRoot, }) builtEntry = { node_id: nodeId, @@ -1247,8 +1250,7 @@ export function handleToolCall(id: string | number | null, graphPath: string, pa if (Object.hasOwn(toolArguments, 'budget') && prBudget === null) { return helpers.failure(id, helpers.jsonrpcInvalidParams, `budget must be a number between 1 and ${helpers.maxStdioTokenBudget}`) } - const graphDir = dirname(validateGraphPath(graphPath)) - const projectRoot = dirname(graphDir) + const projectRoot = resolveGraphSourceRoot(validateGraphPath(graphPath), graph) const prResult = analyzePrImpact(graph, projectRoot, { ...(prBaseBranch ? { baseBranch: prBaseBranch } : {}), ...(prDepth !== null ? { depth: prDepth } : {}), @@ -1343,7 +1345,7 @@ export function handleToolCall(id: string | number | null, graphPath: string, pa ...(retrieveRerankModel ? { rerankerModel: retrieveRerankModel } : {}), ...(retrieveLevelTyped !== null ? { retrievalLevel: retrieveLevelTyped } : {}), ...(effectiveRetrieveStrategy ? { retrievalStrategy: effectiveRetrieveStrategy } : {}), - projectRoot: dirname(resolve(graphPath)), + projectRoot: resolveGraphSourceRoot(graphPath, graph), }) : Promise.resolve(retrieveContext(graph, { question, budget: retrieveBudget, @@ -1534,8 +1536,7 @@ export function handleToolCall(id: string | number | null, graphPath: string, pa if (requireFreshContextInput === true) { return helpers.failure(id, helpers.jsonrpcInvalidParams, 'require_fresh_context is not supported for task=review') } - const graphDir = dirname(validateGraphPath(graphPath)) - const projectRoot = dirname(graphDir) + const projectRoot = resolveGraphSourceRoot(validateGraphPath(graphPath), graph) const prResult = analyzePrImpact(graph, projectRoot, { budget: resolvedBudget, taskIntent: initialPlan.evidence.recipe_id, diff --git a/src/runtime/task-applicability.ts b/src/runtime/task-applicability.ts index 4ef380cb..78ab89dc 100644 --- a/src/runtime/task-applicability.ts +++ b/src/runtime/task-applicability.ts @@ -361,11 +361,42 @@ export function buildPromptApplicabilityHookScript( hookEventName: string, graphPath = 'out/graph.json', ): string { - const graphAccessStatement = graphPath === 'out/graph.json' - ? "fs.accessSync('out/graph.json')" - : `fs.accessSync(${JSON.stringify(graphPath)})` + const graphAvailabilityFunction = graphPath === 'out/graph.json' + ? `function hasMadarGraph() { + let directory = process.cwd() + while (true) { + if (fs.existsSync(path.join(directory, 'out', 'graph.json'))) { + return true + } + + // A linked Git worktree stores Madar artifacts below the common Git + // directory. Its MCP server builds that graph at session startup, so the + // prompt hook should still provide guidance instead of looking for the + // primary checkout's out/ directory. + try { + if (fs.lstatSync(path.join(directory, '.git')).isFile()) { + return true + } + } catch {} + + const parent = path.dirname(directory) + if (parent === directory) { + return false + } + directory = parent + } +}` + : `function hasMadarGraph() { + try { + fs.accessSync(${JSON.stringify(graphPath)}) + return true + } catch { + return false + } +}` return `const fs = require('fs') +const path = require('path') const config = ${JSON.stringify(HOOK_CONFIG, null, 2)} const matchPayload = ${JSON.stringify(matchPayloadJson)} @@ -376,6 +407,8 @@ const githubProjectUrlRe = ${GITHUB_PROJECT_URL_RE} const packageRegistryUrlRe = ${PACKAGE_REGISTRY_URL_RE} let input = '' +${graphAvailabilityFunction} + function normalizePrompt(prompt) { return String(prompt || '') .toLowerCase() @@ -514,9 +547,7 @@ process.stdin.on('data', (chunk) => { }) process.stdin.on('end', () => { - try { - ${graphAccessStatement} - } catch { + if (!hasMadarGraph()) { return } diff --git a/src/shared/graph-source-root.ts b/src/shared/graph-source-root.ts new file mode 100644 index 00000000..9d7bc359 --- /dev/null +++ b/src/shared/graph-source-root.ts @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs' +import { basename, dirname, resolve } from 'node:path' + +import { resolveWorkspaceGraphPath } from './workspace.js' + +interface GraphRootCarrier { + graph?: { + root_path?: unknown + } +} + +/** + * Returns the source workspace recorded in a graph. New graphs always carry + * `root_path`; the path-based fallback preserves compatibility with older + * `/out/graph.json` artifacts. + */ +export function resolveGraphSourceRoot(graphPath: string, graph?: GraphRootCarrier): string { + const storedRoot = graph?.graph?.root_path + if (typeof storedRoot === 'string' && storedRoot.trim().length > 0) { + return resolve(storedRoot.trim()) + } + + const graphDirectory = dirname(resolve(graphPath)) + return basename(graphDirectory) === 'out' ? dirname(graphDirectory) : graphDirectory +} + +/** Reads just enough of graph.json to resolve its recorded source workspace. */ +export function readGraphSourceRoot(graphPath: string): string { + const resolvedGraphPath = resolveWorkspaceGraphPath(graphPath) + try { + const parsed = JSON.parse(readFileSync(resolvedGraphPath, 'utf8')) as { root_path?: unknown } + return resolveGraphSourceRoot(resolvedGraphPath, { graph: { root_path: parsed.root_path } }) + } catch { + return resolveGraphSourceRoot(resolvedGraphPath) + } +} diff --git a/src/shared/security.ts b/src/shared/security.ts index d0ec84af..cc414d63 100644 --- a/src/shared/security.ts +++ b/src/shared/security.ts @@ -2,6 +2,8 @@ import { existsSync, lstatSync, realpathSync } from 'node:fs' import { isIP } from 'node:net' import { dirname, relative, resolve, sep } from 'node:path' +import { resolveWorkspaceGraphPath, resolveWorkspaceOutputPath } from './workspace.js' + const CONTROL_CHAR_RE = /[\x00-\x1f\x7f]/g const MAX_LABEL_LENGTH = 256 const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']) @@ -41,12 +43,13 @@ export function findNearestExistingAncestor(targetPath: string): string | null { } export function validateGraphPath(graphPath: string, base?: string): string { - const resolvedBase = resolve(base ?? inferGraphBase(graphPath)) + const effectiveGraphPath = base === undefined ? resolveWorkspaceGraphPath(graphPath) : graphPath + const resolvedBase = resolve(base ?? inferGraphBase(effectiveGraphPath)) if (!existsSync(resolvedBase)) { throw new Error(`Graph base directory does not exist: ${resolvedBase}. Run madar first to build the graph.`) } - const resolvedPath = resolve(graphPath) + const resolvedPath = resolve(effectiveGraphPath) const basePrefix = resolvedBase.endsWith(sep) ? resolvedBase : `${resolvedBase}${sep}` if (resolvedPath !== resolvedBase && !resolvedPath.startsWith(basePrefix)) { throw new Error(`Path ${JSON.stringify(graphPath)} escapes the allowed directory ${resolvedBase}. Only paths inside out/ are permitted.`) @@ -66,9 +69,12 @@ export function validateGraphPath(graphPath: string, base?: string): string { return realPath } -export function validateGraphOutputPath(targetPath: string, base = 'out'): string { - const resolvedBase = resolve(base) - const resolvedTarget = resolve(targetPath) +export function validateGraphOutputPath(targetPath: string, base = 'out', workspaceRoot = process.cwd()): string { + const usesDefaultOutputBase = base === 'out' + const effectiveBase = usesDefaultOutputBase ? resolveWorkspaceOutputPath(base, workspaceRoot) : base + const effectiveTarget = usesDefaultOutputBase ? resolveWorkspaceOutputPath(targetPath, workspaceRoot) : targetPath + const resolvedBase = resolve(effectiveBase) + const resolvedTarget = resolve(effectiveTarget) const basePrefix = resolvedBase.endsWith(sep) ? resolvedBase : `${resolvedBase}${sep}` if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(basePrefix)) { throw new Error(`Path ${JSON.stringify(targetPath)} escapes the allowed directory ${resolvedBase}. Only paths inside out/ are permitted.`) diff --git a/src/shared/workspace.ts b/src/shared/workspace.ts new file mode 100644 index 00000000..d7ec4b7f --- /dev/null +++ b/src/shared/workspace.ts @@ -0,0 +1,146 @@ +import { createHash } from 'node:crypto' +import { execFileSync } from 'node:child_process' +import { realpathSync } from 'node:fs' +import { join, resolve } from 'node:path' + +export interface MadarWorkspace { + /** Source root Madar is indexing. This may be a directory within a worktree. */ + rootPath: string + /** Git worktree root, when the source root belongs to a Git checkout. */ + worktreeRoot: string | null + /** Shared Git metadata directory for the repository, when available. */ + gitCommonDir: string | null + /** True only for a linked Git worktree, never the primary checkout. */ + isLinkedWorktree: boolean + /** Physical directory that owns this source root's Madar artifacts. */ + artifactRoot: string + outputDir: string + graphPath: string +} + +function canonicalPath(path: string): string { + const resolved = resolve(path) + try { + return realpathSync(resolved) + } catch { + return resolved + } +} + +function gitPath(rootPath: string, args: string[]): string | null { + try { + const value = execFileSync('git', ['-C', rootPath, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }).trim() + return value.length > 0 ? value : null + } catch { + return null + } +} + +function worktreeArtifactId(commonDir: string, worktreeRoot: string, sourceRoot: string): string { + return createHash('sha256') + .update(`${canonicalPath(commonDir)}\u0000${canonicalPath(worktreeRoot)}\u0000${canonicalPath(sourceRoot)}`) + .digest('hex') + .slice(0, 24) +} + +/** + * Resolves where Madar should store artifacts for a source root. + * + * Primary checkouts and non-Git directories keep the established `/out` + * layout. A linked Git worktree receives an isolated artifact directory below + * the repository's common Git directory, keeping generated data outside the + * worktree while ensuring two branches cannot share a graph. + */ +export function resolveMadarWorkspace(rootPath = '.'): MadarWorkspace { + // Keep public paths in the caller's resolved spelling. Canonical paths are + // only for identity/hash comparisons, otherwise macOS /var -> /private/var + // aliases leak into normal non-worktree output paths. + const sourceRoot = resolve(rootPath) + const worktreeValue = gitPath(sourceRoot, ['rev-parse', '--show-toplevel']) + // Ask Git for absolute metadata paths. Relative `--git-common-dir` output + // varies across Git platforms when the source root is nested below a primary + // checkout, and can otherwise make that primary checkout look like a linked + // worktree on Windows. + const commonDirValue = worktreeValue + ? gitPath(sourceRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir']) + : null + const gitDirValue = worktreeValue + ? gitPath(sourceRoot, ['rev-parse', '--path-format=absolute', '--git-dir']) + : null + + const worktreeRoot = worktreeValue ? resolve(worktreeValue) : null + const gitCommonDir = worktreeRoot && commonDirValue ? resolve(commonDirValue) : null + const gitDir = worktreeRoot && gitDirValue ? resolve(gitDirValue) : null + const isLinkedWorktree = gitCommonDir !== null + && gitDir !== null + && canonicalPath(gitCommonDir) !== canonicalPath(gitDir) + + const artifactRoot = isLinkedWorktree && gitCommonDir && worktreeRoot + ? join(gitCommonDir, 'madar', 'worktrees', worktreeArtifactId(gitCommonDir, worktreeRoot, sourceRoot)) + : sourceRoot + const outputDir = join(artifactRoot, 'out') + + return { + rootPath: sourceRoot, + worktreeRoot, + gitCommonDir, + isLinkedWorktree, + artifactRoot, + outputDir, + graphPath: join(outputDir, 'graph.json'), + } +} + +export function resolveMadarOutputDirectory(rootPath = '.'): string { + return resolveMadarWorkspace(rootPath).outputDir +} + +/** + * Resolves the conventional graph path for the active workspace. Explicit + * graph paths are left alone so users can still serve an arbitrary artifact. + */ +export function resolveWorkspaceGraphPath(graphPath = 'out/graph.json', workspaceRoot = process.cwd()): string { + const normalized = graphPath.replaceAll('\\', '/').replace(/^(?:\.\/)+/, '') + if (normalized === 'out/graph.json') { + const workspace = resolveMadarWorkspace(workspaceRoot) + // Preserve the public relative default for normal checkouts. A linked + // worktree is the only case that needs a redirected physical artifact. + return workspace.isLinkedWorktree ? workspace.graphPath : graphPath + } + return graphPath +} + +/** + * Resolves a conventional `out` artifact path for the active workspace. + * + * This intentionally only redirects paths rooted at `out/`. Explicit paths + * remain explicit, while built-in commands can keep using their established + * relative defaults without creating a second `out` directory in a linked + * worktree. + */ +export function resolveWorkspaceOutputPath(outputPath = 'out', workspaceRoot = process.cwd()): string { + const normalized = outputPath.replaceAll('\\', '/').replace(/^(?:\.\/)+/, '') + if (normalized !== 'out' && !normalized.startsWith('out/')) { + return outputPath + } + + const workspace = resolveMadarWorkspace(workspaceRoot) + if (!workspace.isLinkedWorktree) { + return outputPath + } + + if (normalized === 'out') { + return workspace.outputDir + } + + const suffix = normalized.slice('out/'.length).split('/').filter((segment) => segment.length > 0) + return join(workspace.outputDir, ...suffix) +} + +export function isLinkedGitWorktree(rootPath = '.'): boolean { + return resolveMadarWorkspace(rootPath).isLinkedWorktree +} diff --git a/tests/unit/cli.test.ts b/tests/unit/cli.test.ts index a813b1cf..973a97f9 100644 --- a/tests/unit/cli.test.ts +++ b/tests/unit/cli.test.ts @@ -1,5 +1,7 @@ -import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' import { type CliDependencies, executeCli, formatHelp } from '../../src/cli/main.js' import { @@ -30,6 +32,7 @@ import { UsageError, } from '../../src/cli/parser.js' import { KnowledgeGraph } from '../../src/contracts/graph.js' +import { resolveWorkspaceGraphPath } from '../../src/shared/workspace.js' type GraphSummaryPayload = { graph_version?: string @@ -992,6 +995,7 @@ describe('cli parser', () => { host: '127.0.0.1', port: 4173, transport: 'http', + autoRefresh: false, }) expect(parseServeArgs(['custom.json', '--host', '0.0.0.0', '--port', '8080'])).toEqual({ @@ -999,6 +1003,7 @@ describe('cli parser', () => { host: '0.0.0.0', port: 8080, transport: 'http', + autoRefresh: false, }) expect(parseServeArgs(['graph.json', '--mcp'])).toEqual({ @@ -1006,6 +1011,7 @@ describe('cli parser', () => { host: '127.0.0.1', port: 4173, transport: 'stdio', + autoRefresh: false, }) expect(parseServeArgs(['graph.json', '--transport', 'stdio'])).toEqual({ @@ -1013,6 +1019,7 @@ describe('cli parser', () => { host: '127.0.0.1', port: 4173, transport: 'stdio', + autoRefresh: false, }) expect(parseServeArgs(['graph.json', '--http'])).toEqual({ @@ -1020,6 +1027,15 @@ describe('cli parser', () => { host: '127.0.0.1', port: 4173, transport: 'http', + autoRefresh: false, + }) + + expect(parseServeArgs(['--stdio', '--auto-refresh'])).toEqual({ + graphPath: 'out/graph.json', + host: '127.0.0.1', + port: 4173, + transport: 'stdio', + autoRefresh: true, }) expect(() => parseServeArgs(['--port', '70000'])).toThrow('must be between 0 and 65535') @@ -2838,6 +2854,45 @@ describe('cli main', () => { expect(logs).toContain('opencode local rules installed') }) + it('recognizes an external linked-worktree graph before warning during install', async () => { + const sandbox = mkdtempSync(join(tmpdir(), 'madar-cli-worktree-')) + const primary = join(sandbox, 'primary') + const linked = join(sandbox, 'linked') + const originalCwd = process.cwd() + + try { + execFileSync('git', ['init', primary], { stdio: 'pipe' }) + execFileSync('git', ['-C', primary, 'config', 'user.email', 'madar-tests@example.com'], { stdio: 'pipe' }) + execFileSync('git', ['-C', primary, 'config', 'user.name', 'Madar Tests'], { stdio: 'pipe' }) + writeFileSync(join(primary, 'main.ts'), 'export const primary = true\n', 'utf8') + execFileSync('git', ['-C', primary, 'add', '.'], { stdio: 'pipe' }) + execFileSync('git', ['-C', primary, 'commit', '-m', 'initial'], { stdio: 'pipe' }) + execFileSync('git', ['-C', primary, 'worktree', 'add', '-b', 'feature/install-warning', linked], { stdio: 'pipe' }) + + const graphPath = resolveWorkspaceGraphPath('out/graph.json', linked) + mkdirSync(dirname(graphPath), { recursive: true }) + writeFileSync(graphPath, '{}\n', 'utf8') + + process.chdir(linked) + const { io, logs } = createIo() + + await expect(executeCli(['claude', 'install'], io, createDependencies())).resolves.toBe(0) + + expect(logs).toContain('claude local rules installed') + expect(logs.join('\n')).not.toContain('Warning: out/graph.json not found') + } finally { + process.chdir(originalCwd) + if (existsSync(primary)) { + try { + execFileSync('git', ['-C', primary, 'worktree', 'remove', '--force', linked], { stdio: 'pipe' }) + } catch { + // The temporary directory cleanup below handles partial setup. + } + } + rmSync(sandbox, { recursive: true, force: true }) + } + }, 20_000) + it('passes the requested install profile into claude, cursor, gemini, and copilot installs', async () => { const { io } = createIo() const dependencies = createDependencies() @@ -2879,6 +2934,7 @@ describe('cli main', () => { let watched = false let served = false let servedOverStdio = false + let stdioOptions: unknown let lastGenerateOptions: Record | undefined let lastWatchOptions: Record | undefined const dependencies = createDependencies() @@ -2894,13 +2950,14 @@ describe('cli main', () => { dependencies.serveGraph = async () => { served = true } - dependencies.serveGraphStdio = async () => { + dependencies.serveGraphStdio = async (options) => { servedOverStdio = true + stdioOptions = options } const watchExitCode = await executeCli(['watch', 'src', '--respect-gitignore', '--debounce', '1', '--no-html'], io, dependencies) const serveExitCode = await executeCli(['serve', 'out/graph.json', '--port', '0'], io, dependencies) - const stdioExitCode = await executeCli(['serve', 'out/graph.json', '--mcp'], io, dependencies) + const stdioExitCode = await executeCli(['serve', 'out/graph.json', '--mcp', '--auto-refresh'], io, dependencies) expect(watchExitCode).toBe(0) expect(serveExitCode).toBe(0) @@ -2912,6 +2969,7 @@ describe('cli main', () => { expect(lastGenerateOptions?.respectGitignore).toBe(true) expect(lastWatchOptions?.noHtml).toBe(true) expect(lastWatchOptions?.respectGitignore).toBe(true) + expect(stdioOptions).toMatchObject({ graphPath: 'out/graph.json', autoRefresh: true, workspaceRoot: process.cwd() }) expect(logs[0]).toContain('[madar generate]') }) diff --git a/tests/unit/compare-native-agent.test.ts b/tests/unit/compare-native-agent.test.ts index 4db0f9f8..957622f7 100644 --- a/tests/unit/compare-native-agent.test.ts +++ b/tests/unit/compare-native-agent.test.ts @@ -1779,8 +1779,12 @@ describe('executeNativeAgentCompare', () => { } }) - it('records implement-task outcome scoring with isolated per-arm workspaces', async () => { + it('records implement-task outcome scoring with isolated per-arm workspaces and an external graph artifact', async () => { const { projectDir, graphPath, outputDir, questionsPath } = makeImplementationFixtureProject() + const externalGraphRoot = mkdtempSync(join(COMPARE_OUTPUT_PARENT, 'external-graph-')) + const externalGraphPath = join(externalGraphRoot, 'out', 'graph.json') + mkdirSync(dirname(externalGraphPath), { recursive: true }) + writeFileSync(externalGraphPath, readFileSync(graphPath, 'utf8'), 'utf8') try { const runner: NativeAgentRunner = async (input) => { const workspaceRoot = input.cwd ?? projectDir @@ -1826,7 +1830,7 @@ describe('executeNativeAgentCompare', () => { const result = await executeNativeAgentCompare( { - graphPath, + graphPath: externalGraphPath, questionsPath, outputDir, execTemplate: 'mock-runner', @@ -1905,9 +1909,10 @@ describe('executeNativeAgentCompare', () => { expect(summary).toContain('wrong-file edits') expect(summary).toContain('reviewer-visible') } finally { + rmSync(externalGraphRoot, { recursive: true, force: true }) rmSync(projectDir, { recursive: true, force: true }) } - }) + }, 30_000) it('times out implement validation commands instead of hanging after the agent run', async () => { const { projectDir, graphPath, outputDir, questionsPath } = makeImplementationFixtureProject() diff --git a/tests/unit/doctor.test.ts b/tests/unit/doctor.test.ts index f708bcdb..18a8dad5 100644 --- a/tests/unit/doctor.test.ts +++ b/tests/unit/doctor.test.ts @@ -41,12 +41,14 @@ function writeText(path: string, content: string): void { writeFileSync(path, content, 'utf8') } -function writeMcpServer(path: string, serversKey: 'mcpServers' | 'servers', graphPath: string): void { +function writeMcpServer(path: string, serversKey: 'mcpServers' | 'servers', graphPath?: string): void { writeJson(path, { [serversKey]: { madar: { command: 'npx', - args: ['--yes', '@lubab/madar', 'serve', '--stdio', graphPath], + args: graphPath + ? ['--yes', '@lubab/madar', 'serve', '--stdio', graphPath] + : ['--yes', '@lubab/madar', 'serve', '--stdio', '--auto-refresh'], }, }, }) @@ -87,9 +89,9 @@ describe('doctor command', () => { BeforeTool: [{ matcher: 'read_file', hooks: [{ type: 'command', command: 'out' }] }], }, }) - writeMcpServer(resolve(sandboxDir, '.mcp.json'), 'mcpServers', graphPath) - writeMcpServer(resolve(sandboxDir, '.cursor', 'mcp.json'), 'mcpServers', graphPath) - writeMcpServer(resolve(sandboxDir, '.vscode', 'mcp.json'), 'servers', graphPath) + writeMcpServer(resolve(sandboxDir, '.mcp.json'), 'mcpServers') + writeMcpServer(resolve(sandboxDir, '.cursor', 'mcp.json'), 'mcpServers') + writeMcpServer(resolve(sandboxDir, '.vscode', 'mcp.json'), 'servers') const doctor = runDoctorCommand({ projectDir: sandboxDir, diff --git a/tests/unit/install.test.ts b/tests/unit/install.test.ts index 1d74688c..3fd9e235 100644 --- a/tests/unit/install.test.ts +++ b/tests/unit/install.test.ts @@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, relative } from 'node:path' +import { pathToFileURL } from 'node:url' import * as ts from 'typescript' import { @@ -597,6 +598,23 @@ describe('install helpers', () => { }) }) + it('keeps Gemini hook guidance active in a linked worktree without a local out graph', () => { + withTempDir((projectDir) => { + const nestedDirectory = join(projectDir, 'nested', 'agent-session') + mkdirSync(nestedDirectory, { recursive: true }) + writeFileSync(join(projectDir, '.git'), 'gitdir: ../.git/worktrees/linked\n', 'utf8') + geminiInstall(projectDir) + + const settings = readFileSync(join(projectDir, '.gemini', 'settings.json'), 'utf8') + const command = extractHookCommand(settings, 'BeforeTool') + const output = runHookCommand(command, nestedDirectory, { tool_name: 'read_file' }) + + expect(command).toContain('madar-workspace-graph-check') + expect(output).toContain('additionalContext') + expect(output).toContain('madar knowledge graph') + }) + }) + it('fails loudly for malformed existing Gemini JSON config files', () => { withTempDir((projectDir) => { const settingsPath = join(projectDir, '.gemini', 'settings.json') @@ -856,7 +874,7 @@ describe('install helpers', () => { expect(mcpConfig.mcpServers?.['madar']?.args).toEqual([ 'serve', '--stdio', - join(projectDir, 'out', 'graph.json'), + '--auto-refresh', ]) }) }) @@ -955,7 +973,7 @@ describe('install helpers', () => { expect(mcpConfig.mcpServers?.['madar']?.args).toEqual([ 'serve', '--stdio', - join(projectDir, 'out', 'graph.json'), + '--auto-refresh', ]) }) }) @@ -1064,7 +1082,7 @@ describe('install helpers', () => { normalizeAssertionPath(cliPath), 'serve', '--stdio', - normalizeAssertionPath(join(projectDir, 'out', 'graph.json')), + '--auto-refresh', ]) }) }) @@ -1210,7 +1228,7 @@ describe('install helpers', () => { expect(opencodeConfig.plugin).toContain('.opencode/plugins/madar.js') expect(opencodeConfig.mcp?.madar).toEqual({ type: 'local', - command: [process.execPath, cliPath, 'serve', '--stdio', join(projectDir, 'out', 'graph.json')], + command: [process.execPath, cliPath, 'serve', '--stdio', '--auto-refresh'], enabled: true, }) }) @@ -1294,7 +1312,7 @@ describe('install helpers', () => { }) }) - it('uses an absolute graph path when the Codex prompt hook runs from a nested directory', () => { + it('finds the workspace graph when the Codex prompt hook runs from a nested directory', () => { withTempDir((temporaryDir) => { const projectDir = join(temporaryDir, 'repo-$()-`tick`-$HOME') mkdirSync(join(projectDir, 'out'), { recursive: true }) @@ -1316,7 +1334,7 @@ describe('install helpers', () => { { prompt: 'Explain how this repository auth module works' }, ) - expect(hookScript).toContain(JSON.stringify(join(projectDir, 'out', 'graph.json'))) + expect(hookScript).toContain('function hasMadarGraph()') expect(command).not.toContain(projectDir) expect(JSON.parse(output)).toMatchObject({ hookSpecificOutput: { @@ -1374,6 +1392,38 @@ describe('install helpers', () => { }) }) + it('activates the OpenCode reminder in a linked worktree without a local out graph', async () => { + const projectDir = mkdtempSync(join(tmpdir(), 'madar-opencode-worktree-')) + const packageRoot = mkdtempSync(join(tmpdir(), 'madar-opencode-package-')) + + try { + const cliPath = join(packageRoot, PACKAGE_CLI_RELATIVE_PATH) + mkdirSync(dirname(cliPath), { recursive: true }) + writeFileSync(join(packageRoot, 'package.json'), JSON.stringify({ name: 'madar-test', bin: { madar: PACKAGE_CLI_RELATIVE_PATH } }), 'utf8') + writeFileSync(cliPath, '#!/usr/bin/env node\n', 'utf8') + writeFileSync(join(projectDir, 'package.json'), JSON.stringify({ type: 'module' }), 'utf8') + writeFileSync(join(projectDir, '.git'), 'gitdir: ../.git/worktrees/linked\n', 'utf8') + + agentsInstall(projectDir, 'opencode', { packageRoot }) + + const pluginPath = join(projectDir, '.opencode', 'plugins', 'madar.js') + const pluginModule = await import(pathToFileURL(pluginPath).href) as { + MadarPlugin: (context: { directory: string }) => Promise<{ + 'tool.execute.before': (input: { tool: string }, output: { args: { command: string } }) => Promise + }> + } + const plugin = await pluginModule.MadarPlugin({ directory: join(projectDir, 'nested', 'agent-session') }) + const output = { args: { command: 'pwd' } } + + await plugin['tool.execute.before']({ tool: 'bash' }, output) + + expect(output.args.command).toContain('[madar] Knowledge graph available.') + } finally { + rmSync(projectDir, { recursive: true, force: true }) + rmSync(packageRoot, { recursive: true, force: true }) + } + }) + it('migrates recognized legacy Codex PreToolUse hooks while preserving user hooks', () => { withTempDir((projectDir) => { const stalePayload = JSON.stringify({ @@ -1536,9 +1586,8 @@ describe('install helpers', () => { it('writes an idempotent marker-owned Codex MCP block while preserving unrelated TOML and line endings', () => { withTempDir((projectDir) => { const configPath = join(projectDir, '.codex', 'config.toml') - const graphPath = join(projectDir, 'out', 'graph.json') const unrelatedToml = '# Preserve this user comment\r\n[features]\r\nparallel = true\r\n' - const managedBlock = `${CODEX_MCP_START_MARKER}\r\n[mcp_servers.madar]\r\ncommand = "madar"\r\nargs = ["serve", "--stdio", ${JSON.stringify(graphPath)}]\r\nenv = { MADAR_TOOL_PROFILE = "core" }\r\nenabled = true\r\n${CODEX_MCP_END_MARKER}\r\n` + const managedBlock = `${CODEX_MCP_START_MARKER}\r\n[mcp_servers.madar]\r\ncommand = "madar"\r\nargs = ["serve", "--stdio", "--auto-refresh"]\r\nenv = { MADAR_TOOL_PROFILE = "core" }\r\nenabled = true\r\n${CODEX_MCP_END_MARKER}\r\n` mkdirSync(join(projectDir, '.codex'), { recursive: true }) writeFileSync(configPath, unrelatedToml, 'utf8') @@ -1627,7 +1676,6 @@ ${CODEX_MCP_END_MARKER} it('rewrites only a complete owned Codex MCP marker block', () => { withTempDir((projectDir) => { const configPath = join(projectDir, '.codex', 'config.toml') - const graphPath = join(projectDir, 'out', 'graph.json') const before = `# before\n${CODEX_MCP_START_MARKER}\n[mcp_servers.madar]\ncommand = "old-madar"\nargs = ["old"]\n${CODEX_MCP_END_MARKER}\n# after\n` mkdirSync(join(projectDir, '.codex'), { recursive: true }) @@ -1640,7 +1688,7 @@ ${CODEX_MCP_END_MARKER} expect(installed).toContain('# before\n') expect(installed).toContain('# after\n') expect(installed).toContain('[mcp_servers.madar]\ncommand = "madar"') - expect(installed).toContain(`args = ["serve", "--stdio", ${JSON.stringify(graphPath)}]`) + expect(installed).toContain('args = ["serve", "--stdio", "--auto-refresh"]') expect(installed).toContain('env = { MADAR_TOOL_PROFILE = "core" }') expect(installed).toContain('enabled = true') expect(installed).not.toContain('old-madar') @@ -1758,7 +1806,7 @@ ${CODEX_MCP_END_MARKER} expect(opencodeConfig.mcp?.other).toEqual({ type: 'remote', url: 'https://example.com/mcp' }) expect(opencodeConfig.mcp?.madar).toEqual({ type: 'local', - command: [process.execPath, cliPath, 'serve', '--stdio', join(projectDir, 'out', 'graph.json')], + command: [process.execPath, cliPath, 'serve', '--stdio', '--auto-refresh'], enabled: true, environment: { HTTP_PROXY: 'http://proxy.example' }, }) @@ -1808,7 +1856,7 @@ ${CODEX_MCP_END_MARKER} expect(installedConfig.mcp?.other).toEqual({ type: 'remote', url: 'https://example.com/mcp' }) expect(installedConfig.mcp?.madar).toEqual({ type: 'local', - command: [process.execPath, cliPath, 'serve', '--stdio', join(projectDir, 'out', 'graph.json')], + command: [process.execPath, cliPath, 'serve', '--stdio', '--auto-refresh'], enabled: true, }) diff --git a/tests/unit/mcp-registry-metadata.test.ts b/tests/unit/mcp-registry-metadata.test.ts index 0cf96170..2fa674ad 100644 --- a/tests/unit/mcp-registry-metadata.test.ts +++ b/tests/unit/mcp-registry-metadata.test.ts @@ -100,16 +100,9 @@ describe('MCP Registry metadata', () => { expect(npmPackage?.packageArguments?.map((entry) => entry.value)).toEqual([ 'serve', '--stdio', - undefined, + '--auto-refresh', ]) - expect(graphPathArgument).toMatchObject({ - type: 'positional', - valueHint: 'graph_path', - default: 'out/graph.json', - format: 'filepath', - isRequired: true, - }) - expect(graphPathArgument?.description?.toLowerCase()).toContain('madar generate') + expect(graphPathArgument).toBeUndefined() expect(toolProfile).toMatchObject({ name: 'MADAR_TOOL_PROFILE', default: 'core', @@ -117,6 +110,7 @@ describe('MCP Registry metadata', () => { expect(toolProfile?.choices).toEqual(expect.arrayContaining(['core', 'full'])) expect(publisherNotes?.source).toBe('docs/mcp-registry/server.json') expect(publisherNotes?.notes).toContain('Madar is the renamed continuation of `graphify-ts`') + expect(publisherNotes?.notes).toContain('`madar serve --stdio --auto-refresh`') expect(publisherNotes?.notes).toContain('`@lubab/madar`') expect(publisherNotes?.notes).toContain('`https://github.com/mohanagy/madar`') }) @@ -174,6 +168,7 @@ describe('MCP Registry metadata', () => { expect(reference).toContain('docs/mcp-registry/server.json') expect(reference).toContain('npm run registry:validate') expect(reference).toContain('The official MCP Registry hosts metadata, not Madar code or your local graph artifact.') + expect(reference).toContain('`npx @lubab/madar serve --stdio --auto-refresh`') expect(reference).toContain('Private registry usage stays out of scope for the public Madar listing') expect(reference).toContain('If you still discover older `graphify-ts` links or listings, Madar is the current project name.') expect(reference).toContain('`https://github.com/mohanagy/madar`') diff --git a/tests/unit/mcp-response-evidence.test.ts b/tests/unit/mcp-response-evidence.test.ts index b4cb27a1..57fd9b8c 100644 --- a/tests/unit/mcp-response-evidence.test.ts +++ b/tests/unit/mcp-response-evidence.test.ts @@ -1,3 +1,7 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + import { describe, expect, it } from 'vitest' import { buildMadarResponseEvidence } from '../../src/runtime/mcp-response-evidence.js' @@ -187,6 +191,38 @@ describe('mcp-response-evidence', () => { ) }) + it('uses the recorded source root when a graph artifact lives outside its worktree', () => { + const root = mkdtempSync(join(tmpdir(), 'madar-external-graph-evidence-')) + const sourceRoot = join(root, 'linked-worktree', 'backend') + const graphPath = join(root, 'git-artifacts', 'worktree', 'out', 'graph.json') + try { + mkdirSync(dirname(graphPath), { recursive: true }) + writeFileSync(graphPath, JSON.stringify({ root_path: sourceRoot }), 'utf8') + + const evidence = buildMadarResponseEvidence({ + graphPath, + coveredWorkflowOwners: ['backend/src/runtime.ts'], + executionSlice: { + status: 'complete', + confidence: 'high', + confidence_reasons: [], + steps: [], + phase_coverage: { + expected: [], + observed: [], + missing: [], + }, + }, + }) + + expect(evidence.confidence_reasons).toContain( + 'scope quality: graph scope is aligned with the backend runtime evidence', + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + it('does not mark runtime-generation answers as contained when no execution slice exists', () => { const evidence = buildMadarResponseEvidence({ graphPath: 'backend/out/graph.json', diff --git a/tests/unit/pipeline.test.ts b/tests/unit/pipeline.test.ts index 7ef2c933..e5cd1810 100644 --- a/tests/unit/pipeline.test.ts +++ b/tests/unit/pipeline.test.ts @@ -111,7 +111,10 @@ function runPipeline(tempDir: string) { } describe('pipeline', () => { - const referenceFixturesTimeoutMs = 30_000 + // The full CI matrix runs this CPU-heavy reference corpus beside other + // extraction suites. It can exceed the normal 30s deadline under parallel + // load without indicating a correctness regression. + const referenceFixturesTimeoutMs = 90_000 it('runs end to end on the reference fixtures', () => { withTempDir((tempDir) => { @@ -127,14 +130,14 @@ describe('pipeline', () => { expect(first.graph.numberOfNodes()).toBe(second.graph.numberOfNodes()) expect(first.graph.numberOfEdges()).toBe(second.graph.numberOfEdges()) }) - }) + }, referenceFixturesTimeoutMs) it('mentions the top god node in the generated report', () => { withTempDir((tempDir) => { const result = runPipeline(tempDir) expect(result.report).toContain(`\`${escapeMarkdownInline(result.gods[0]?.label ?? '')}\``) }) - }) + }, referenceFixturesTimeoutMs) it('detects both code and docs in the fixture corpus', () => { withTempDir((tempDir) => { @@ -143,7 +146,7 @@ describe('pipeline', () => { expect(result.detection.files.document.length).toBeGreaterThan(0) expect(result.extraction.nodes.some((node) => node.file_type === 'document')).toBe(true) }) - }) + }, referenceFixturesTimeoutMs) it('keeps extraction confidence labels within the expected set', () => { withTempDir((tempDir) => { @@ -153,7 +156,7 @@ describe('pipeline', () => { expect(valid.has(edge.confidence)).toBe(true) } }) - }) + }, referenceFixturesTimeoutMs) it('does not introduce self loops into the built graph', () => { withTempDir((tempDir) => { @@ -162,5 +165,5 @@ describe('pipeline', () => { expect(source).not.toBe(target) } }) - }) + }, referenceFixturesTimeoutMs) }) diff --git a/tests/unit/stdio-pr-impact.test.ts b/tests/unit/stdio-pr-impact.test.ts index ccaa8c37..d97da4b9 100644 --- a/tests/unit/stdio-pr-impact.test.ts +++ b/tests/unit/stdio-pr-impact.test.ts @@ -1,5 +1,5 @@ import { execFileSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -364,6 +364,31 @@ describe('stdio pr impact', () => { expect(payload.review_bundle.token_count).toBeLessThanOrEqual(240) }) + it('uses graph root_path for pr impact when the graph artifact lives outside the repository', async () => { + const root = createRepo() + const artifactRoot = mkdtempSync(join(tmpdir(), 'madar-stdio-external-artifact-')) + repoRoots.push(root, artifactRoot) + writeFileSync( + join(root, 'src', 'auth.ts'), + readFileSync(join(root, 'src', 'auth.ts'), 'utf8').replace(' const status = "ok"', ' const status = token.startsWith("Bearer ") ? "ok" : "fail"'), + 'utf8', + ) + mkdirSync(join(artifactRoot, 'out')) + const graphPath = join(artifactRoot, 'out', 'graph.json') + copyFileSync(join(root, 'out', 'graph.json'), graphPath) + + const response = await Promise.resolve(handleStdioRequest(graphPath, { + id: 3, + method: 'tools/call', + params: { name: 'pr_impact', arguments: { budget: 240 } }, + })) + const payload = JSON.parse((response?.result as { content: Array<{ text: string }> }).content[0]!.text) + + expect(payload.seed_nodes).toEqual([ + expect.objectContaining({ label: 'authenticateUser', match_kind: 'line' }), + ]) + }) + it('returns the compact pr_impact payload by default and the full payload for verbose or compact=false', async () => { const root = createRepo({ reviewHeavy: true }) repoRoots.push(root) diff --git a/tests/unit/stdio-server.test.ts b/tests/unit/stdio-server.test.ts index 7d8143c5..26c884e1 100644 --- a/tests/unit/stdio-server.test.ts +++ b/tests/unit/stdio-server.test.ts @@ -1,5 +1,5 @@ import { execFileSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' import { PassThrough } from 'node:stream' import { join, resolve } from 'node:path' import { setTimeout as delay } from 'node:timers/promises' @@ -9,6 +9,17 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { handleStdioRequest, serveGraphStdio } from '../../src/runtime/stdio-server.js' import { graphFreshnessMetadata } from '../../src/runtime/freshness.js' +async function waitFor(condition: () => boolean, timeoutMs = 5_000): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (condition()) { + return + } + await delay(10) + } + throw new Error('Timed out waiting for expected condition') +} + function createGraphFixtureRoot(): string { const parentDir = resolve('out', 'test-runtime') mkdirSync(parentDir, { recursive: true }) @@ -2576,6 +2587,70 @@ describe('stdio runtime', () => { } }) + it('refreshes an active stdio session after an agent changes its workspace', async () => { + const parentDir = resolve('out', 'test-runtime') + mkdirSync(parentDir, { recursive: true }) + const root = mkdtempSync(join(parentDir, 'madar-stdio-auto-refresh-')) + const graphPath = join(root, 'out', 'graph.json') + const input = new PassThrough() + const output = new PassThrough() + const errorOutput = new PassThrough() + let outputText = '' + output.on('data', (chunk) => { + outputText += chunk.toString('utf8') + }) + + writeFileSync(join(root, 'initial.ts'), 'export const initialValue = 1\n', 'utf8') + const serverPromise = serveGraphStdio({ + graphPath, + autoRefresh: true, + workspaceRoot: root, + autoRefreshDebounceSeconds: 0.02, + input, + output, + errorOutput, + }) + + try { + await waitFor(() => { + if (!existsSync(graphPath)) { + return false + } + const graph = JSON.parse(readFileSync(graphPath, 'utf8')) as { nodes?: Array<{ source_file?: string }> } + return graph.nodes?.some((node) => node.source_file?.endsWith('initial.ts')) === true + }) + + input.write(`${JSON.stringify({ id: 1, method: 'stats' })}\n`) + await waitFor(() => outputText.includes('"id":1')) + + writeFileSync(join(root, 'added.ts'), 'export function addedDuringSession() { return 2 }\n', 'utf8') + await waitFor(() => { + const graph = JSON.parse(readFileSync(graphPath, 'utf8')) as { nodes?: Array<{ source_file?: string }> } + return graph.nodes?.some((node) => node.source_file?.endsWith('added.ts')) === true + }) + + input.end(`${JSON.stringify({ id: 2, method: 'stats' })}\n`) + await serverPromise + + const responses = outputText + .trim() + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)) as Array<{ id?: number; result?: string }> + const before = responses.find((response) => response.id === 1) + const after = responses.find((response) => response.id === 2) + const refreshedGraph = JSON.parse(readFileSync(graphPath, 'utf8')) as { nodes?: unknown[] } + + expect(before?.result).toContain('Nodes: 1') + expect(after?.result).toContain(`Nodes: ${refreshedGraph.nodes?.length ?? 0}`) + expect(after?.result).not.toBe(before?.result) + } finally { + input.destroy() + await serverPromise.catch(() => {}) + rmSync(root, { recursive: true, force: true }) + } + }, 10_000) + it('returns JSON-RPC-style errors for invalid requests', () => { const root = createGraphFixtureRoot() try { diff --git a/tests/unit/time-travel-infrastructure.test.ts b/tests/unit/time-travel-infrastructure.test.ts index 86314df3..891b928b 100644 --- a/tests/unit/time-travel-infrastructure.test.ts +++ b/tests/unit/time-travel-infrastructure.test.ts @@ -1,14 +1,27 @@ -import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' +import { existsSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { basename, join, relative, resolve, sep } from 'node:path' +import { tmpdir } from 'node:os' +import { execFileSync } from 'node:child_process' import { afterEach, describe, expect, it, vi } from 'vitest' import { EXTRACTOR_CACHE_VERSION } from '../../src/pipeline/extract.js' import { compareRefs, loadOrBuildSnapshot, type CompareRefsDependencies, type SnapshotDependencies } from '../../src/infrastructure/time-travel.js' import type { TimeTravelResult } from '../../src/runtime/time-travel.js' +import { resolveMadarWorkspace } from '../../src/shared/workspace.js' const createdRoots = new Set() +function isInside(candidate: string, root: string): boolean { + const relativePath = relative(root, candidate) + return relativePath === '' || (!relativePath.startsWith('..') && !relativePath.startsWith(`..${sep}`)) +} + +function normalizedGitPath(path: string): string { + const canonical = realpathSync.native(path).replaceAll('\\', '/') + return process.platform === 'win32' ? canonical.toLowerCase() : canonical +} + function createDeferred(): { promise: Promise resolve: (value: T | PromiseLike) => void @@ -224,6 +237,65 @@ describe('time travel infrastructure', () => { await expect(loadOrBuildSnapshot({ ref: 'HEAD', refresh: false }, deps)).rejects.toThrow('build failed') }) + it('keeps linked-worktree snapshots isolated and removes the transient external artifact', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'madar-time-travel-worktree-')) + const primary = join(tempDir, 'primary') + const linked = join(tempDir, 'linked') + try { + execFileSync('git', ['init', primary], { stdio: 'pipe' }) + execFileSync('git', ['config', 'user.email', 'madar-tests@example.com'], { cwd: primary, stdio: 'pipe' }) + execFileSync('git', ['config', 'user.name', 'Madar Tests'], { cwd: primary, stdio: 'pipe' }) + writeFileSync(join(primary, 'main.ts'), 'export const snapshotValue = 1\n') + execFileSync('git', ['add', '.'], { cwd: primary, stdio: 'pipe' }) + execFileSync('git', ['commit', '-m', 'initial'], { cwd: primary, stdio: 'pipe' }) + execFileSync('git', ['worktree', 'add', '-b', 'feature/time-travel', linked], { cwd: primary, stdio: 'pipe' }) + + const linkedWorkspace = resolveMadarWorkspace(linked) + const materializedWorktrees: string[] = [] + const result = await loadOrBuildSnapshot({ ref: 'HEAD' }, { + rootDir: linked, + git: { + createDetachedWorktree(worktreePath, commitSha): void { + materializedWorktrees.push(worktreePath) + execFileSync('git', ['worktree', 'add', '--detach', worktreePath, commitSha], { cwd: linked, stdio: 'pipe' }) + }, + removeWorktree(worktreePath): void { + execFileSync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: linked, stdio: 'pipe' }) + }, + }, + }) + const artifactContainer = join(linkedWorkspace.gitCommonDir ?? '', 'madar', 'worktrees') + + expect(linkedWorkspace.isLinkedWorktree).toBe(true) + expect(result.graphPath).toBe(join(linkedWorkspace.outputDir, 'time-travel', 'snapshots', result.commitSha, 'graph.json')) + expect(existsSync(result.graphPath)).toBe(true) + expect(existsSync(join(linked, 'out'))).toBe(false) + expect(readdirSync(artifactContainer).sort()).toEqual([basename(linkedWorkspace.artifactRoot)]) + expect(materializedWorktrees).toHaveLength(1) + const [materializedWorktree] = materializedWorktrees + if (!materializedWorktree) { + throw new Error('Expected one transient time-travel worktree') + } + expect(isInside(materializedWorktree, linkedWorkspace.gitCommonDir ?? '')).toBe(false) + expect(existsSync(materializedWorktree)).toBe(false) + + const worktreeList = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: linked, encoding: 'utf8', stdio: 'pipe' }) + const normalizedWorktreeList = process.platform === 'win32' ? worktreeList.toLowerCase() : worktreeList + expect(normalizedWorktreeList).toContain(`worktree ${normalizedGitPath(primary)}`) + expect(normalizedWorktreeList).toContain(`worktree ${normalizedGitPath(linked)}`) + expect(worktreeList).not.toContain('time-travel/worktrees') + } finally { + if (existsSync(primary)) { + try { + execFileSync('git', ['worktree', 'remove', '--force', linked], { cwd: primary, stdio: 'pipe' }) + } catch { + // Temp directory cleanup below still handles partial setup failures. + } + } + rmSync(tempDir, { recursive: true, force: true }) + } + }, 20_000) + it('loads both snapshots and compares them through the runtime helper', async () => { const rootDir = createTestRoot('compare') writeCachedSnapshot(rootDir, 'from-sha') diff --git a/tests/unit/watch.test.ts b/tests/unit/watch.test.ts index 7e428c00..963b4ab1 100644 --- a/tests/unit/watch.test.ts +++ b/tests/unit/watch.test.ts @@ -1,12 +1,12 @@ import { execFileSync } from 'node:child_process' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, utimesSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { setTimeout as delay } from 'node:timers/promises' import { describe, expect, test, vi } from 'vitest' -import { WATCHED_EXTENSIONS, hasNonCode, notifyOnly, rebuildCode, watch } from '../../src/infrastructure/watch.js' +import { WATCHED_EXTENSIONS, hasNonCode, notifyOnly, rebuildCode, startGraphAutoRefresh, watch } from '../../src/infrastructure/watch.js' import { generateGraph } from '../../src/infrastructure/generate.js' import { binaryIngestSidecarPath } from '../../src/shared/binary-ingest-sidecar.js' @@ -108,9 +108,64 @@ describe('rebuildCode', () => { expect(existsSync(join(tempDir, 'out', 'graph.json'))).toBe(true) }) }) + + test('keeps the existing SPI build profile during an automatic refresh', () => { + withTempDir((tempDir) => { + const sourcePath = join(tempDir, 'main.ts') + writeFileSync(sourcePath, 'export const original = true\n', 'utf8') + generateGraph(tempDir, { useSpi: true, noHtml: true }) + + writeFileSync(sourcePath, 'export const refreshed = true\n', 'utf8') + expect(rebuildCode(tempDir, { noHtml: true })).toBe(true) + + const graph = JSON.parse(readFileSync(join(tempDir, 'out', 'graph.json'), 'utf8')) as { spi_mode?: unknown } + expect(graph.spi_mode).toBe(true) + }) + }) + + test('recovers a stale refresh lease left by a dead process', () => { + withTempDir((tempDir) => { + writeFileSync(join(tempDir, 'main.ts'), 'export const refreshed = true\n', 'utf8') + const outputDir = join(tempDir, 'out') + mkdirSync(outputDir, { recursive: true }) + const lockPath = join(outputDir, '.madar-refresh.lock') + writeFileSync(lockPath, '999999999 stale-lease 1970-01-01T00:00:00.000Z\n', 'utf8') + const staleAt = new Date(Date.now() - (2 * 60 * 60 * 1000)) + utimesSync(lockPath, staleAt, staleAt) + + expect(rebuildCode(tempDir, { noHtml: true })).toBe(true) + expect(existsSync(lockPath)).toBe(false) + }) + }) }) describe('watch', () => { + test('reconciles at MCP startup and refreshes a later agent edit', async () => { + await withTempDirAsync(async (tempDir) => { + writeFileSync(join(tempDir, 'main.ts'), 'export const initialValue = 1\n', 'utf8') + const refresh = startGraphAutoRefresh(tempDir, 0.02, { + pollIntervalMs: 10, + noHtml: true, + logger: { log() {}, error() {} }, + }) + + try { + const graphPath = join(tempDir, 'out', 'graph.json') + expect(refresh.initialRebuilt).toBe(true) + expect(existsSync(graphPath)).toBe(true) + + writeFileSync(join(tempDir, 'added.ts'), 'export function addedDuringSession() { return 2 }\n', 'utf8') + await waitFor(() => { + const graph = JSON.parse(readFileSync(graphPath, 'utf8')) as { nodes?: Array<{ source_file?: string }> } + return graph.nodes?.some((node) => node.source_file?.endsWith('added.ts')) === true + }) + } finally { + refresh.stop() + await refresh.completed + } + }) + }, 10_000) + test('triggers a Git-visible rebuild when .gitignore changes', async () => { await withTempDirAsync(async (tempDir) => { writeFileSync(join(tempDir, 'main.ts'), 'export const visible = true\n', 'utf8') @@ -586,3 +641,14 @@ async function withTempDirAsync(callback: (tempDir: string) => Promise): P rmSync(tempDir, { recursive: true, force: true }) } } + +async function waitFor(condition: () => boolean, timeoutMs = 5_000): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (condition()) { + return + } + await delay(25) + } + throw new Error('Timed out waiting for graph refresh') +} diff --git a/tests/unit/workspace.test.ts b/tests/unit/workspace.test.ts new file mode 100644 index 00000000..02d892ce --- /dev/null +++ b/tests/unit/workspace.test.ts @@ -0,0 +1,105 @@ +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, relative, resolve, sep } from 'node:path' + +import { describe, expect, test } from 'vitest' + +import { generateGraph } from '../../src/infrastructure/generate.js' +import { validateGraphOutputPath } from '../../src/shared/security.js' +import { resolveMadarWorkspace, resolveWorkspaceGraphPath, resolveWorkspaceOutputPath } from '../../src/shared/workspace.js' + +function git(directory: string, args: string[]): void { + execFileSync('git', args, { cwd: directory, stdio: 'pipe' }) +} + +function isInside(candidate: string, root: string): boolean { + const relativePath = relative(root, candidate) + return relativePath === '' || (!relativePath.startsWith('..') && !relativePath.startsWith(`..${sep}`)) +} + +function canonicalPhysicalPath(path: string): string { + const canonical = realpathSync.native(path) + return process.platform === 'win32' ? canonical.toLowerCase() : canonical +} + +describe('worktree artifact routing', () => { + test('keeps a nested source root in a primary checkout local', () => { + const tempDir = mkdtempSync(join(tmpdir(), 'madar-primary-workspace-')) + const primary = join(tempDir, 'primary') + const nested = join(primary, 'packages', 'api') + try { + execFileSync('git', ['init', primary], { stdio: 'pipe' }) + mkdirSync(nested, { recursive: true }) + + const workspace = resolveMadarWorkspace(nested) + + expect(canonicalPhysicalPath(workspace.worktreeRoot ?? '')).toBe(canonicalPhysicalPath(primary)) + expect(workspace.isLinkedWorktree).toBe(false) + expect(workspace.outputDir).toBe(join(resolve(nested), 'out')) + expect(workspace.graphPath).toBe(join(resolve(nested), 'out', 'graph.json')) + } finally { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + test('keeps a linked worktree graph outside the source checkout and isolated from the primary checkout', () => { + const tempDir = mkdtempSync(join(tmpdir(), 'madar-worktree-')) + const primary = join(tempDir, 'primary') + const linked = join(tempDir, 'linked') + try { + execFileSync('git', ['init', primary], { stdio: 'pipe' }) + git(primary, ['config', 'user.email', 'madar-tests@example.com']) + git(primary, ['config', 'user.name', 'Madar Tests']) + writeFileSync(join(primary, 'main.ts'), 'export const primaryValue = 1\n', 'utf8') + git(primary, ['add', '.']) + git(primary, ['commit', '-m', 'initial']) + git(primary, ['worktree', 'add', '-b', 'feature/worktree-routing', linked]) + mkdirSync(join(linked, 'src')) + + const primaryWorkspace = resolveMadarWorkspace(primary) + const linkedWorkspace = resolveMadarWorkspace(linked) + const scopedWorkspace = resolveMadarWorkspace(join(linked, 'src')) + + expect(primaryWorkspace.isLinkedWorktree).toBe(false) + expect(primaryWorkspace.graphPath).toBe(join(resolve(primary), 'out', 'graph.json')) + expect(linkedWorkspace.isLinkedWorktree).toBe(true) + expect(canonicalPhysicalPath(linkedWorkspace.gitCommonDir ?? '')).toBe(canonicalPhysicalPath(join(primary, '.git'))) + expect(linkedWorkspace.graphPath).not.toBe(primaryWorkspace.graphPath) + expect(isInside(linkedWorkspace.graphPath, linked)).toBe(false) + expect(scopedWorkspace.graphPath).not.toBe(linkedWorkspace.graphPath) + expect(resolveWorkspaceGraphPath('out/graph.json', linked)).toBe(linkedWorkspace.graphPath) + expect(resolveWorkspaceGraphPath('./out/graph.json', linked)).toBe(linkedWorkspace.graphPath) + expect(resolveWorkspaceOutputPath('out/compare', linked)).toBe(join(linkedWorkspace.outputDir, 'compare')) + expect(validateGraphOutputPath('out/compare', 'out', linked)).toBe(join(linkedWorkspace.outputDir, 'compare')) + + writeFileSync(join(linked, 'feature.ts'), 'export function worktreeOnlyFeature() { return 2 }\n', 'utf8') + const result = generateGraph(linked, { noHtml: true }) + const graph = JSON.parse(readFileSync(result.graphPath, 'utf8')) as { root_path?: string; nodes?: Array<{ source_file?: string }> } + + expect(result.outputDir).toBe(linkedWorkspace.outputDir) + expect(result.graphPath).toBe(linkedWorkspace.graphPath) + expect(existsSync(join(linked, 'out'))).toBe(false) + expect(graph.root_path).toBe(resolve(linked)) + expect(graph.nodes?.some((node) => node.source_file?.endsWith('feature.ts'))).toBe(true) + + writeFileSync(join(linked, 'feature.ts'), 'export function worktreeOnlyFeature() { return 3 }\n', 'utf8') + const update = generateGraph(linked, { update: true, noHtml: true }) + expect(update.outputDir).toBe(linkedWorkspace.outputDir) + + const spi = generateGraph(linked, { useSpi: true, noHtml: true }) + expect(spi.outputDir).toBe(linkedWorkspace.outputDir) + expect(existsSync(join(linked, 'out'))).toBe(false) + expect(existsSync(join(linkedWorkspace.outputDir, '.spi-cache'))).toBe(true) + } finally { + if (existsSync(primary)) { + try { + git(primary, ['worktree', 'remove', '--force', linked]) + } catch { + // The temp directory cleanup below is still safe if setup failed. + } + } + rmSync(tempDir, { recursive: true, force: true }) + } + }, 20_000) +}) diff --git a/tests/unit/worktree-cli-artifacts.test.ts b/tests/unit/worktree-cli-artifacts.test.ts new file mode 100644 index 00000000..373f7479 --- /dev/null +++ b/tests/unit/worktree-cli-artifacts.test.ts @@ -0,0 +1,97 @@ +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, test } from 'vitest' + +import { parseCompareArgs, parseProofReportArgs, parseReviewCompareArgs } from '../../src/cli/parser.js' +import { KnowledgeGraph } from '../../src/contracts/graph.js' +import { runProofReportCommand } from '../../src/infrastructure/proof-report.js' +import { toJson } from '../../src/pipeline/export.js' +import { resolveMadarWorkspace } from '../../src/shared/workspace.js' + +function git(directory: string, args: string[]): void { + execFileSync('git', args, { cwd: directory, stdio: 'pipe' }) +} + +describe('linked-worktree CLI artifact routing', () => { + test('derives compare, review, and proof artifacts from the external graph directory', () => { + const tempDir = mkdtempSync(join(tmpdir(), 'madar-worktree-cli-')) + const primary = join(tempDir, 'primary') + const linked = join(tempDir, 'linked') + const originalCwd = process.cwd() + + try { + execFileSync('git', ['init', primary], { stdio: 'pipe' }) + git(primary, ['config', 'user.email', 'madar-tests@example.com']) + git(primary, ['config', 'user.name', 'Madar Tests']) + writeFileSync(join(primary, 'main.ts'), 'export const value = 1\n', 'utf8') + git(primary, ['add', '.']) + git(primary, ['commit', '-m', 'initial']) + git(primary, ['worktree', 'add', '-b', 'feature/cli-artifacts', linked]) + + const workspace = resolveMadarWorkspace(linked) + mkdirSync(dirname(workspace.graphPath), { recursive: true }) + const graph = new KnowledgeGraph() + graph.graph.root_path = linked + graph.addNode('entry', { + label: 'value', + source_file: 'main.ts', + source_location: 'L1', + node_kind: 'variable', + file_type: 'code', + }) + toJson(graph, { 0: ['entry'] }, workspace.graphPath) + + process.chdir(linked) + + expect(parseCompareArgs([ + 'where is value defined?', + '--exec', + 'claude -p "$(cat {prompt_file})"', + ])).toMatchObject({ + graphPath: workspace.graphPath, + outputDir: join(workspace.outputDir, 'compare'), + }) + expect(parseReviewCompareArgs([ + '--exec', + 'claude -p "$(cat {prompt_file})"', + ])).toMatchObject({ + graphPath: workspace.graphPath, + outputDir: join(workspace.outputDir, 'review-compare'), + }) + expect(parseProofReportArgs([])).toEqual({ + graphPath: workspace.graphPath, + outputDir: join(workspace.outputDir, 'proof-report'), + compareDir: join(workspace.outputDir, 'compare'), + packPath: null, + }) + expect(parseProofReportArgs([ + '--output-dir', 'out/proof-report/custom', + '--compare-dir', 'out/compare/custom', + '--pack', 'out/proof-inputs/context-pack.json', + ])).toEqual({ + graphPath: workspace.graphPath, + outputDir: join(workspace.outputDir, 'proof-report', 'custom'), + compareDir: join(workspace.outputDir, 'compare', 'custom'), + packPath: join(workspace.outputDir, 'proof-inputs', 'context-pack.json'), + }) + + const proof = runProofReportCommand({ graphPath: 'out/graph.json' }) + expect(proof.outputPath).toBe(join(workspace.outputDir, 'proof-report', 'proof-report.md')) + expect(existsSync(proof.outputPath)).toBe(true) + expect(existsSync(join(linked, 'out'))).toBe(false) + } finally { + process.chdir(originalCwd) + if (existsSync(primary)) { + try { + git(primary, ['worktree', 'remove', '--force', linked]) + } catch { + // Temp cleanup below handles partially-created worktrees too. + } + } + rmSync(tempDir, { recursive: true, force: true }) + } + }) +})