Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ source navigation. Suspended frames provide lazy Locals/Parameters scopes,
typed expandable values, expression evaluation, and native watches, including
caller-frame inspection within the daemon's supported limits.

Run and Debug accept explicitly selected lowercase `.fql` regular files beneath
discovery-excluded directories such as `.tmp` and `testdata`, including nested
Go modules. Selection preserves the original source identity and compilation
workspace; it does not discover neighboring excluded files. Workspace containment
and the daemon's nested-symlink restrictions still apply.

## Protocol schemas

[`ferretd.json`](ferretd.json) is the sole version pin for both the daemon
Expand Down
9 changes: 9 additions & 0 deletions extensions/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ when one exists, otherwise the canonical source parent. A present project base
must be a readable directory containing the source; the plugin does not fall
back when it is invalid. Relative configured paths require a project base.

The bundled daemon accepts explicitly selected lowercase `.fql` regular files
inside discovery-excluded directories such as `.tmp`, `testdata`, and nested Go
modules. For example, a configuration targeting `.tmp/test.fql` runs and debugs
that file within the project workspace, preserving its source identity. Automatic
discovery exclusions remain in effect for neighboring files; workspace containment
and the daemon's nested-symlink restrictions are unchanged. Plugins bundled with
`ferretd 1.0.0-alpha.8` require updating to a build containing alpha.9 or later
to use this capability.

The runtime working directory is resolved and validated independently. It may
be outside the workspace and does not participate in source containment or
relative source identity. Leaving it blank omits the execution option, causing
Expand Down
6 changes: 4 additions & 2 deletions extensions/jetbrains/docs/debugger-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Current status (September 22, 2026): M3 T2/T3 implement the selected native
XDebugger → LSP4J Debug → per-launch `ferretd dap` architecture against the sole
repository pin `1.0.0-alpha.8`. Run and Debug share launch-input resolution;
repository pin `1.0.0-alpha.9`. Run and Debug share launch-input resolution;
breakpoints, control commands, stacks, source navigation, console output, and
session cleanup are implemented. M3 T3 adds lazy Locals/Parameters groups, typed
values, selected-frame expression evaluation, and native watches. Inspection
Expand All @@ -15,7 +15,9 @@ historical evidence, including its alpha.6 blockers and then-future task lists;
it is not the current capability summary.

Alpha.8 fixes the first-executable-statement breakpoint failure recorded with
alpha.7. The [debugger validation report](debugger-validation.md) retains that
alpha.7. Alpha.9 admits explicitly selected sources beneath discovery-excluded
directories, including `.tmp`, without changing the source path or workspace.
The [debugger validation report](debugger-validation.md) retains that
historical evidence and records follow-up validation. The manual UI pass
remains unverified, so T2 is not an unconditional release sign-off.

Expand Down
87 changes: 87 additions & 0 deletions extensions/jetbrains/docs/debugger-validation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,92 @@
# JetBrains debugger validation

## September 22, 2026: alpha.9 explicit-file admission

The sole repository pin is now `1.0.0-alpha.9`. The
[upstream release](https://github.com/MontFerret/ferretd/releases/tag/v1.0.0-alpha.9)
includes explicit source admission and the macOS and Windows test-fixture
corrections from PR #28. The alpha.8-to-alpha.9 release diff
contains no protobuf changes. Both supported generators ran against the new
schemas and produced no client diff.

Run and Debug now accept explicitly selected lowercase `.fql` regular files
beneath discovery-excluded directories and nested Go modules. Source identity,
the compilation workspace, containment, nested-symlink restrictions, and
immutable active-session snapshots retain their existing contracts. Editorium
adds no admission logic, path rewriting, source copies, or workspace fallback.

### Regression evidence

The native Run test, both native Debug tests, and both native inspection tests
now retain `.tmp` fixtures permanently. They cover saving edited documents,
source navigation, live breakpoint controls, caller-frame selection, recursive
values, expression evaluation across stops, concurrent sessions, project lifetime
cancellation, and relaunch. The selected-frame fixture uses `.tmp/test.fql`;
ordinary-directory cases remain in the existing real-daemon suites.

Two additional real-daemon tests each exercise `.tmp/test.fql`,
`testdata/test.fql`, and `module/test.fql` beneath a nested `go.mod`.
Run checks the returned Session workspace ID, relative path, original canonical
source URI, results, saved edits, and daemon reuse. DAP checks a verified
breakpoint, original stack source, locals, evaluation, final output, and adapter
termination. Both read a root-owned file with no runtime-directory override;
different contents in each source parent detect an accidental workspace fallback.

The seven focused alpha.9 tests passed. Against retained alpha.8, both new
tests failed on `.tmp/test.fql`: DAP returned `workspace document not found`,
and Run reported `Ferret source was not found.` The complete subsequent
alpha.9 suite passed. The first focused attempt also corrected a test expectation
to use JetBrains' canonical macOS path rather than its temporary-directory alias;
no production path conversion changed.

### Automated validation

Local environment: macOS arm64, Go 1.26.5, Temurin JDK 25.0.4.1 for Gradle,
Node.js 26.5.1, and IntelliJ Platform 2026.2.0.1 (`IU-262.8665.337`).
These results do not claim execution on the CI Node.js 22 environment or on
Linux/Windows hosts.

| Check | Result |
| --- | --- |
| `make prepare jetbrains` | PASS; all six alpha.9 release archives checksum-verified and staged. |
| `make proto-sync FORCE=1`; both `proto-generate` and `proto-check` targets | PASS; generated Java and TypeScript clients unchanged. |
| `go test ./...` in `tools/editorium` | PASS. |
| `make test jetbrains` | PASS; 173 unit/platform tests and 29 real-daemon integration tests, zero failures, errors, or skips. |
| `make lint jetbrains` | PASS; Plugin Verifier reports compatibility with IU-262.8665.337. |
| `make build jetbrains` | PASS; universal build and structure validation. |
| `make package jetbrains`; `make package-check jetbrains` | PASS; complete six-target matrix, alpha.9 version marker, binary bytes, Unix modes, and native executable version checked. |
| `make test vscode` | PASS; 138 unit tests, 13 integration tests, and daemon execution transport smoke. |
| `make lint vscode`; `make build vscode` | PASS. |
| `make package vscode`; `make package-check vscode` | PASS; native darwin-arm64 VSIX. |
| Final diff review | PASS; ownership, cleanup, source identity, coverage, generated output, documentation, and whitespace reviewed. |

The universal artifact is
`extensions/jetbrains/build/distributions/ferret-jetbrains-0.1.0.zip`.
The native VSIX is
`extensions/vscode/dist/ferret-vscode-0.1.0-darwin-arm64.vsix`.
Other-host daemon binaries were validated as package contents, not executed.
No plugin version was changed, no extension release was published, and the
user's installed plugins were not replaced. Installed alpha.8 bundles remain
affected until updated to a build containing the correction.

### Manual IDE attempt

An isolated target-version IDE was launched with disposable system/config/plugin
directories and a project containing `.tmp/test.fql`, an inspection configuration,
and an ordinary `normal.fql` control. Its log confirms
`IU-262.8665.337` and `Loaded custom plugins: Ferret (0.1.0)`; the sandbox's
daemon version marker is `1.0.0-alpha.9`.

Computer control listed **IntelliJ IDEA Alpha9 Sandbox**, but selecting either
`com.jetbrains.jbr.java` or the displayed name returned **Invalid app**.
The owned launch was interrupted and exited with code 130. Manual Run, Debug,
breakpoints, caller selection, expansion, watches, controls, termination, and
relaunch remain **UNVERIFIED**. Automated native-platform results above do not
substitute for that UI matrix.

The alpha.8 reports below preserve their original versions, test counts, and
manual limitations; their pin statements describe those historical runs.

## September 22, 2026: M3 T3 inspection and hardening

M3 T3 implements lazy native scopes and recursive typed values, selected-frame
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.ferretlang.jetbrains.integration

import com.google.gson.JsonParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -122,6 +123,37 @@ class FerretDapIntegrationTest {
}
}

@Test fun debugsExplicitExcludedSourcesWithOriginalIdentityAndWorkspace() = runBlocking {
Files.writeString(root.resolve("value.txt"), "project workspace")
Files.createDirectories(root.resolve("module"))
Files.writeString(root.resolve("module/go.mod"), "module example.com/nested\n")
for (relativePath in listOf(".tmp/test.fql", "testdata/test.fql", "module/test.fql")) {
withTimeout(15_000) {
val source = root.resolve(relativePath)
Files.createDirectories(source.parent)
Files.writeString(source.parent.resolve("value.txt"), "wrong source parent")
val launch = launch(relativePath, "LET value = 7\nRETURN {value, root: TO_STRING(IO::FS::READ(\"value.txt\"))}", lines = intArrayOf(2))
launch.session.launchCompleted.await()
assertTrue(relativePath, launch.listener.replacements.receive().second.single().isVerified)
val stop = launch.stopped()
assertEquals("breakpoint", stop.reason)
assertEquals(listOf(2L), stop.hitKeys)
val frame = requireNotNull(launch.session.stackTrace(stop, 0, 1)).stackFrames.single()
assertEquals(source.toString(), frame.source.path)
assertEquals(2, frame.line)
val locals = requireNotNull(launch.session.scopes(stop, frame.id)).scopes.single { it.name == "Locals" }
val value = requireNotNull(launch.session.variables(stop, locals.variablesReference)).variables.single { it.name == "value" }
assertEquals("7", value.value)
assertEquals("8", requireNotNull(launch.session.evaluate(stop, frame.id, "value + 1")).result)
launch.session.command(FerretDapCommand.CONTINUE, stop)
assertEquals(0, launch.session.completion.await())
assertEquals(JsonParser.parseString("""{"value":7,"root":"project workspace"}"""), JsonParser.parseString(launch.listener.output.receive().first))
assertTrue(launch.listener.errors.tryReceive().isFailure)
assertFalse(launch.process.isAlive)
}
}
}

@Test fun hitsVerifiedBreakpointAtFirstExecutableStatement() = runBlocking {
withTimeout(15_000) {
val launch = launch("first-statement.fql", "LET value = 1\nRETURN value", lines = intArrayOf(1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,40 @@ class FerretExecutionIntegrationTest {
assertTrue(runtimeFailure.stderr.any { it.contains("missing parameter", ignoreCase = true) })
}

@Test
fun executesExplicitExcludedSourcesWithOriginalIdentityAndWorkspace() = runBlocking {
write("value.txt", "project workspace")
write("module/go.mod", "module example.com/nested\n")
for (relativePath in listOf(".tmp/test.fql", "testdata/test.fql", "module/test.fql")) {
val source = write(relativePath, "RETURN {value: 7, root: TO_STRING(IO::FS::READ(\"value.txt\"))}")
Files.writeString(source.parent.resolve("value.txt"), "wrong source parent")
val first = execute(source, workingDirectory = null)
val firstExit = first.exit.awaitResult()
assertEquals(relativePath + ": " + first.debug(), 0, firstExit)
assertEquals(JsonParser.parseString("""{"value":7,"root":"project workspace"}"""), JsonParser.parseString(first.stdout.single()))
assertTrue(first.debug.contains("Source: ${source.toRealPath()}"))
assertTrue(first.debug.contains("Workspace root: ${root.toRealPath()}"))

val generation = connection.generation()
val workspaceId = connection.workspace(generation, root.toRealPath())
val session = generation.rpc.createSession(workspaceId, relativePath)
try {
assertEquals(workspaceId, session.workspaceId)
assertEquals(relativePath, session.relativePath)
assertEquals(source.toRealPath(), Path.of(java.net.URI(session.uri)))
} finally {
generation.rpc.closeSession(session.id)
}

Files.writeString(source, "RETURN 8")
val edited = execute(source, workingDirectory = null)
val editedExit = edited.exit.awaitResult()
assertEquals(relativePath + ": " + edited.debug(), 0, editedExit)
assertEquals(JsonParser.parseString("8"), JsonParser.parseString(edited.stdout.single()))
assertEquals(generation.number, connection.generation().number)
}
}

@Test
fun cancelsAndIsolatesConcurrentRuns() = runBlocking {
val first = execute(write("first.fql", "WAIT(10s)\nRETURN 1"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class FerretNativeDebugIntegrationTest : HeavyPlatformTestCase() {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
project.replaceService(FerretDebugLauncher::class.java,
FerretDebugLauncher.testing(scope, Path.of(requireNotNull(System.getenv("FERRETD_TEST_PATH")))), testRootDisposable)
val path = Path.of(requireNotNull(project.basePath)).resolve("native-live.fql")
val path = Path.of(requireNotNull(project.basePath)).resolve(".tmp/test.fql")
Files.createDirectories(path.parent)
Files.writeString(path, "RETURN FOR i IN 1..1000000\n LET delay = WAIT(10)\n LET value = i + 1\n RETURN value")
val file = requireNotNull(LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path))
Expand Down Expand Up @@ -98,7 +98,7 @@ class FerretNativeDebugIntegrationTest : HeavyPlatformTestCase() {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val launcher = FerretDebugLauncher.testing(scope, Path.of(requireNotNull(System.getenv("FERRETD_TEST_PATH"))))
project.replaceService(FerretDebugLauncher::class.java, launcher, testRootDisposable)
val path = Path.of(requireNotNull(project.basePath)).resolve("native-debug.fql")
val path = Path.of(requireNotNull(project.basePath)).resolve(".tmp/test.fql")
Files.createDirectories(path.parent)
Files.writeString(path, "RETURN missing\n\nRETURN missing")
val file = requireNotNull(LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class FerretNativeInspectionIntegrationTest : HeavyPlatformTestCase() {
}

fun testSelectedFrameEvaluationAndVariablesRefreshAcrossStepsAndStops() {
val session = launch("frames", 2)
val session = launch("test", 2)
val stack = requireNotNull(session.suspendContext?.activeExecutionStack)
val frames = frames(stack)
assertEquals(listOf("inner", "outer", "<main>"), frames.map { (it as FerretStackFrame).name })
Expand Down Expand Up @@ -124,8 +124,8 @@ class FerretNativeInspectionIntegrationTest : HeavyPlatformTestCase() {

private fun launch(name: String, input: Int): XDebugSession {
val root = Path.of(requireNotNull(project.basePath))
Files.createDirectories(root)
val path = Files.writeString(root.resolve("$name.fql"), SOURCE)
Files.createDirectories(root.resolve(".tmp"))
val path = Files.writeString(root.resolve(".tmp/$name.fql"), SOURCE)
val file = requireNotNull(LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path))
val manager = XDebuggerManager.getInstance(project)
val type = XDebuggerUtil.getInstance().findBreakpointType(FerretBreakpointType::class.java)
Expand All @@ -142,6 +142,7 @@ class FerretNativeInspectionIntegrationTest : HeavyPlatformTestCase() {
// This fixture invokes the builder directly, outside ExecutionManager's process-start notification.
result.session.debugProcess.processHandler.startNotify()
awaitStop(result.session, null)
assertEquals(file, result.session.currentPosition?.file)
return result.session
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class FerretNativeRunIntegrationTest : BasePlatformTestCase() {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val connection = FerretdDaemonConnection.testing(scope, FerretdInstallation(executable, version))
project.replaceService(FerretdDaemonConnection::class.java, connection, testRootDisposable)
val path = Path.of(requireNotNull(project.basePath)).resolve("native-unsaved.fql")
val path = Path.of(requireNotNull(project.basePath)).resolve(".tmp/test.fql")
Files.createDirectories(path.parent)
Files.writeString(path, "RETURN missing")
val file = requireNotNull(LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path))
Expand Down
6 changes: 6 additions & 0 deletions extensions/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ provides them. Execution-time diagnostics stay in that output rather than being
added to **Problems**, which continues to reflect live language-server analysis.
Different files may execute concurrently.

Run and Debug accept explicitly selected lowercase `.fql` regular files beneath
discovery-excluded directories such as `.tmp`, `testdata`, and nested Go modules.
The original source identity and workspace are preserved. Selecting one file
does not discover excluded neighbors or relax workspace containment and the
daemon's nested-symlink restrictions.

## Bundled ferretd

The extension includes a compatible `ferretd` executable. No separate daemon
Expand Down
Loading
Loading