You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
langs: C# adapter — Roslyn token-stream hashing via the SDK's own csc, dotnet test runner (#95)
- .cs routes to CSharpAdapter; hash helper compiled once per SDK version
with the SDK's bundled Roslyn (no packages, no network), cached in the
user cache dir keyed by SDK version + helper source
- dotnet test with the VSTest FullyQualifiedName~ filter (xUnit/NUnit/
MSTest); file-stem class convention, Outer+Inner nested spelling
- identity: dotnet <sdk-version> + deps suffix over packages.lock.json /
Directory.Packages.props
- LanguageAdapter contract: .cs added to the routing invariant, status
confirmed -> inferred pending review (hash unchanged: invariants are
hash-free)
- CI installs the .NET 9 SDK; ISSUES.md #23 files the example-project and
NUnit/MSTest/MTP live-coverage follow-ups
Co-authored-by: dt <david@e3.com.au>
Copy file name to clipboardExpand all lines: ISSUES.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,3 +35,4 @@ launch follow-ups, with status as of the 0.1.0 release (shipped 2026-06-23).
35
35
20.**Dep-source discovery is root-only**: the dependency-set suffix probes fixed filenames at the project root (matching the runner-detection precedent). Monorepo/multi-file shapes escape it: `go.work`, pnpm/yarn workspaces, Maven child modules, `requirements.txt``-r`/`-c` includes (only the root file is hashed), `go.mod``replace ... => ../local` directives (local replaces never enter `go.sum`), Gradle version catalogs (`gradle/libs.versions.toml` — versions referenced from `build.gradle` live outside the hashed file). All lean unsound-quiet (a change that should bust doesn't) — revocation covers discoveries; recursive/workspace-aware discovery is a conscious future change.
36
36
21.**Strict resolved-env grain**: the identity hashes the *declared* set (the committed lockfile), not the *installed* environment. An opt-in stricter grain (e.g. `importlib.metadata` enumeration of the resolved interpreter) would catch venv-disagrees-with-lockfile drift, but any key-affecting per-client knob permanently partitions the shared cache by knob population — the reason config knobs were rejected for the base feature. If built, it must be a project-level (committed) setting, not client config.
37
37
22.**`serve --python` verify/status asymmetry** (pre-existing): the MCP server threads `--python` into `api.verify` but `api.status` calls `toolchain_identity(root)` with no override, so under a version-different override `status` can call dirty what `verify` just greened. The dependency-set suffix is override-independent, so it cannot widen this gap — but the version core can. Fix candidates: thread the override into `build_server`'s status closure, or report both identities.
38
+
23.**C# example project and live-runner breadth**: the C# adapter shipped with a hermetic xUnit e2e in `tests/test_csharp_adapter.py`, but there is no `examples/csharp-*` project (Go/TS/Java each have one) and no live NUnit/MSTest run in the suite — both ride the same VSTest `FullyQualifiedName~` filter and console-logger output as xUnit, so they're expected-compatible but unproven, the same gap #13 tracks for vitest/jest. Also untested live: xunit v3 / Microsoft.Testing.Platform projects, whose `dotnet test` output format differs (failures there surface as `tests_failed_to_run`, conservative). Follow-up: add an example project, plus an NUnit fixture and an MTP-output parse case.
Copy file name to clipboardExpand all lines: README.md
+6-1Lines changed: 6 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -106,7 +106,7 @@ A contract an agent reverse-engineers from existing code can declare that it has
106
106
107
107
- **Contract hash**: sha256 over a canonical form: keys sorted, whitespace normalised, comments stripped, example order preserved, dep order ignored. `impl`, `tests`, `invariants`, and `status` are excluded, so **relocating files never invalidates**, rewording an invariant is free, and confirming an inferred contract never invalidates anything. Invariants are documentation, not a machine obligation; the real check is the tests, whose source is in the verification key.
108
108
- **Impl hash**: sha256 over the normalised AST of the implementation, so reformatting and comment edits never bust the cache. Docstrings are stripped too.
109
-
- **Verification key**: `(contract hash, impl hash, test-source hash, toolchain identity, transitive dep contract hashes)`. Hashloom caches verification results, keyed so that a change to any contract in the closure, to the implementation, to a test's own source, or to the toolchain identity forces a re-run. The identity is the toolchain version (`python 3.11.7`, `go 1.21.5`, `node <v> ts <v>`, `java 21.0.3`) plus, when the project commits a dependency source at its root (`uv.lock`, `go.sum`, `package-lock.json`, `pom.xml`, ...), a ` deps <file>=<hash>` digest of it — so a green from an environment with a different declared dependency set is never trusted, and a 3.11 pass is never served to 3.13. The grain is the *committed declared* set (never OS/arch — CI greens still serve every platform); a venv that disagrees with its own lockfile is outside the key, which is one of the things shared-cache revocation exists for. Failures are never served from cache. Two caveats. A cached pass assumes deterministic tests, so a green result that depended on wall-clock time, network, or randomness can outlive the condition that made it pass. And the test-source hash covers each test function's own normalised AST, not the conftest fixtures or helpers it calls, so changing only those will not force a re-run yet (see [Roadmap](ROADMAP.md)).
109
+
- **Verification key**: `(contract hash, impl hash, test-source hash, toolchain identity, transitive dep contract hashes)`. Hashloom caches verification results, keyed so that a change to any contract in the closure, to the implementation, to a test's own source, or to the toolchain identity forces a re-run. The identity is the toolchain version (`python 3.11.7`, `go 1.21.5`, `node <v> ts <v>`, `java 21.0.3`, `dotnet 9.0.303`) plus, when the project commits a dependency source at its root (`uv.lock`, `go.sum`, `package-lock.json`, `pom.xml`, ...), a ` deps <file>=<hash>` digest of it — so a green from an environment with a different declared dependency set is never trusted, and a 3.11 pass is never served to 3.13. The grain is the *committed declared* set (never OS/arch — CI greens still serve every platform); a venv that disagrees with its own lockfile is outside the key, which is one of the things shared-cache revocation exists for. Failures are never served from cache. Two caveats. A cached pass assumes deterministic tests, so a green result that depended on wall-clock time, network, or randomness can outlive the condition that made it pass. And the test-source hash covers each test function's own normalised AST, not the conftest fixtures or helpers it calls, so changing only those will not force a re-run yet (see [Roadmap](ROADMAP.md)).
110
110
111
111
## MCP tools (the entire surface)
112
112
@@ -153,6 +153,11 @@ semantics hold per language:
153
153
`pom.xml`routes to Maven, `build.gradle`/`build.gradle.kts` to Gradle, and a
154
154
committed `mvnw`/`gradlew` wrapper is preferred over the PATH binary. Needs a
155
155
JDK >= 11 plus Maven or Gradle; config key `{"java": "..."}`.
156
+
- **C#** (`.cs`): hashes via a Roslyn token-stream helper that hashloom compiles
157
+
once per SDK version with the SDK's *own* `csc` against its bundled
158
+
`Microsoft.CodeAnalysis`(no packages, no network), runs `dotnet test` on the
159
+
root project/solution with a `FullyQualifiedName~` filter (xUnit, NUnit, and
160
+
MSTest all ride it). Needs the .NET SDK >= 8; config key `{"dotnet": "..."}`.
156
161
157
162
Python stays the default; a project can mix languages freely.
Copy file name to clipboardExpand all lines: contracts/LanguageAdapter.yaml
+2-3Lines changed: 2 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@ name: LanguageAdapter
2
2
signature: "Protocol: per-language impl/test-source hashing, toolchain resolution, and test running — six methods, one adapter per impl extension"
3
3
deps: [HashloomError]
4
4
invariants:
5
-
- adapter_for routes on the impl path's extension (.go, .ts/.tsx/.mts/.cts, .java) with Python as the default — one lazy singleton per language, no new contract syntax
5
+
- adapter_for routes on the impl path's extension (.go, .ts/.tsx/.mts/.cts, .java, .cs) with Python as the default — one lazy singleton per language, no new contract syntax
6
6
- impl_hash is formatting/comment/doc-neutral and behaviour-sensitive; a missing file or name raises impl_not_found, unparseable source impl_syntax_error
7
7
- test_source_hash never raises — unresolvable node ids degrade to their literal id, and node-id order never changes the hash
8
8
- "toolchains resolve per language (override, then the .hashloom/config.json key, then PATH/auto-detect) and identities are machine-independent strings: the toolchain version plus, when a well-known lockfile/manifest exists at the project root, a ` deps <file>=<sha256-12>` suffix over its CRLF-normalised bytes — never OS/arch, so cross-OS greens share whenever the committed dependency set matches; a project with no dependency source keeps the version-only identity, so absence never busts keys"
0 commit comments