# Memo 15: Portable OELS distribution and editor path
## Status
Implemented as documentation plus a resolver, capability check, and one
LSP fixture under the ignored `work/` tree. No absolute sibling path is
committed, no dependency is added to the academy, no CI gate is
introduced, and no course content or Quarto publishing pipeline is
altered.
## Purpose
Follow-up to `memo13.md` (inventory) and `memo14.md` (metadata migration).
Give every academy author a reproducible way to obtain and launch the
pinned Open Engineering Language Server (OELS) locally, from any
LSP-capable editor, without hardcoding a specific developer's checkout
path anywhere in the repository.
## Pinned OELS source and build invocation
- **Canonical source:** .
- **Pinned commit (memo15 baseline):** `6016c77007d3f84f4aaac25b30952599ba0f05af`
(short: `6016c77`, message: `Add pnpm lockfile`). All statements about
supported behavior below refer to this SHA.
- **Runtime prerequisite:** Node.js ≥ 20 (the OELS package declares
`engines.node >= 20`). Use of the system Node or a per-developer
version manager is a developer decision; the academy does not ship a
Node install.
- **Reproducible build (per developer, one time per pin):**
```
git clone https://github.com/open-engineering-language-server/source.git oels
cd oels
git checkout 6016c77007d3f84f4aaac25b30952599ba0f05af
corepack enable && corepack prepare pnpm@12.4.1 --activate
pnpm install --frozen-lockfile
pnpm --filter @open-engineering/lsp exec tsc -b
```
Result: an executable bin shim at
`packages/lsp/bin/open-engineering-lsp.mjs` and a built
`packages/lsp/dist/standalone.js`. Neither path is committed to the
academy repository.
- **Rebuild trigger:** whenever this memo advances the pinned SHA, or
the OELS `package.json` version changes upstream, developers rerun the
build. There is no cached artifact in this repository.
## Portable binary resolution
The pinned OELS server always launches over stdio. The resolver enforces
one binary-lookup contract that VS Code, Neovim, Zed, Helix, agent
clients, and the availability check all share:
1. If `OELS_SERVER_PATH` is set:
- if it points at an executable file, use it verbatim
(typical: `.../packages/lsp/bin/open-engineering-lsp.mjs`);
- else if it ends in `.mjs`/`.js` and is readable, launch it as
`node ` (Node ≥ 20 must be on `PATH`);
- else fail with a clear diagnostic.
2. Otherwise, if `open-engineering-lsp` is on `PATH` (published npm
binary, homebrew formula, or a per-developer shim), use it.
3. Otherwise, fail with an actionable message.
`OELS_SERVER_PATH` is set in each developer's shell profile or launcher
environment. It is **never** committed to `_quarto.yml`, per-course
config, editor settings, CI, or any tracked memo other than as
placeholder documentation.
Reference resolver: [`work/oels/resolve-oels.sh`](work/oels/resolve-oels.sh)
(ignored; every editor client and script defers to it so all consumers
agree on resolution semantics). Exit codes: `0` prints the resolved
command, `2` prints one diagnostic to stderr.
## Editor-neutral LSP semantics
OELS speaks LSP over stdio. The academy contract for every client:
- Launch: ` --stdio`.
- Workspace root: the LSP client sends `workspaceFolders` (or
`rootUri`) at `initialize`. Opening the academy repository folder or
a single course folder is sufficient; no per-course override is
needed. OELS derives its workspace root from that folder and looks
for `/definitions/` (top-level only, non-recursive).
- Language IDs to associate with the client: `open-engineering`,
`yaml`, `json`. OELS decides whether to treat a document as an OE
resource from its content (per the five rules in
`templates/README.qmd § What OELS treats as an OE resource`), not
from the languageId.
- Server capabilities exposed by the pinned build: `textDocumentSync`
(incremental), `completionProvider`, `hoverProvider`,
`definitionProvider`, `referencesProvider`, `documentSymbolProvider`,
`workspaceSymbolProvider`. Server info: `open-engineering-lsp`
version `0.0.0`.
- Diagnostics: OELS publishes `textDocument/publishDiagnostics` after
`initialize` and on every `didOpen`/`didChange`, per the memo14
diagnostic contract. Clients must handle empty-diagnostic re-publishes
to clear previously-reported problems.
### Optional VS Code client (example only, not committed)
Any developer using VS Code MAY drop this into their **user**
`settings.json` (never a committed `.vscode/settings.json`):
```jsonc
{
// Preferred: leave empty when a bundled or on-PATH binary is
// available. Fall back to a per-developer env var:
// "openEngineering.server.path": "${env:OELS_SERVER_PATH}"
"openEngineering.server.path": "",
"openEngineering.trace.server": "off"
}
```
The VS Code client shipped from the OELS repository (`packages/vscode`)
is an optional convenience client. Zed, Neovim, Helix, and future
agent/CLI consumers reach the same semantics through the same stdio
entrypoint; the academy places no VS Code-specific requirement on
authors.
## Safe capability/availability check
Reference check: [`work/oels/check-oels.py`](work/oels/check-oels.py)
(ignored; stdlib-only Python). It:
1. Resolves the command via `resolve-oels.sh`.
2. Spawns the OELS server over stdio.
3. Runs the LSP `initialize` handshake against a configurable workspace
root and reports server name/version and advertised capabilities.
4. Sends `textDocument/didOpen` for one deterministic fixture
(`work/oels-contract/fixtures/lsp/malformed-name.yaml`, root
`apiVersion: open-engineering.io/v1alpha1`, `kind: Course`,
`metadata.name: BadName.WithDots`) and asserts that at least one
`textDocument/publishDiagnostics` for that URI is received.
5. Requests `shutdown`, sends `exit`, and terminates.
Invocation:
```
export OELS_SERVER_PATH=/path/to/oels/packages/lsp/bin/open-engineering-lsp.mjs
python3 work/oels/check-oels.py --workspace courses/pico
```
Exit codes: `0` on success, `2` on resolution failure, `3` on missing
`initialize` response, `4` on missing `publishDiagnostics`, `5` on
zero diagnostics for the fixture. All error output is a single line to
stderr; the check never opens a socket, contacts the network, or reads
credentials.
## Supported vs not supported (as of pinned SHA `6016c77`)
Supported today:
- Standalone stdio LSP server via `--stdio`.
- `initialize` with a workspace root plus `initialized`, `didOpen`,
`didChange`, `didClose`, `shutdown`, `exit` lifecycle.
- Diagnostics for OE-shaped resources per the memo14 code set
(`MalformedResource`, `MalformedIdentifier`, `UnknownDefinition`,
`MissingRequiredProperty`, `UnknownProperty`, `IncorrectType`,
`InvalidEnumValue`).
- Completion, hover, definition, references, document symbols, and
workspace symbols (per exposed server capabilities).
- Workspace resource discovery of `.yaml`/`.yml`/`.json` under the
supplied root, with `node_modules`, `.git`, `dist`, `build`, and
dotfile directories pruned.
- Definition discovery at `/definitions/` (top-level
only, non-recursive).
Not supported today (deliberate exclusions or upstream gaps):
- Any transport other than stdio. `--node-ipc`, `--socket`, `--pipe`
cause the standalone entrypoint to exit with code `2`.
- No published npm binary or homebrew formula exists yet; PATH-based
resolution requires a developer-managed shim until upstream ships a
release. The resolver's PATH branch is future-proofing, not the
current default.
- No published semantic version. The server advertises `serverInfo`
version `0.0.0`; the academy pin is the git SHA above.
- No cross-document reference resolution against real academy `id`
graphs (memo14 documents this same gap for the metadata adapter).
- No repository-wide `/definitions/` directory has been
added to the academy yet; `UnknownDefinition` on OE-shaped files is
therefore the expected behavior against the academy root.
- No `.ttl`, `.md`, `.qmd`, `.html`, or Quarto-generated output is
scanned. OELS diagnostics never affect `quarto render`.
- No credentials, network calls, hosted service, or Kubernetes-mediated
runtime are involved in basic authoring.
## Verification commands
Run from the academy repository root:
```
# Resolver: prints one command, or exits 2 with a clear message.
work/oels/resolve-oels.sh
# End-to-end handshake plus one diagnostic against a course workspace.
export OELS_SERVER_PATH=/path/to/oels/packages/lsp/bin/open-engineering-lsp.mjs
python3 work/oels/check-oels.py --workspace courses/pico --timeout 45
# Expected final line: "check-oels: OK"
# No committed sibling absolute path anywhere in tracked files.
grep -RIn "geographical-tuna" . \
':!work' ':!node_modules' ':!.git' ':!_site' ':!_freeze' ':!.quarto'
```
Recorded evidence for the pinned SHA on the author's machine:
- Resolver returned `.../packages/lsp/bin/open-engineering-lsp.mjs` when
`OELS_SERVER_PATH` was set to that shim; exited `2` with the expected
diagnostic in the unset and bogus-path cases; produced `node `
when the shim was made non-executable and re-supplied as an `.mjs`.
- `python3 work/oels/check-oels.py --workspace courses/pico` completed
with `check-oels: OK`, reporting `serverInfo=open-engineering-lsp
version=0.0.0` and capabilities `[completionProvider,
definitionProvider, documentSymbolProvider, hoverProvider,
referencesProvider, textDocumentSync, workspaceSymbolProvider]`, and
received exactly one diagnostic for the fixture URI.
- `git status --short` was clean before and after all runs; the
`work/` tree and the fixture are covered by the existing `/work/`
ignore rule (same rule already used by the memo14 adapter).
- Full-repository-root scans against the pinned build did not complete
within a 30 s handshake timeout on the author's machine; running the
check against a single course workspace (as documented above) is the
recommended verification path today. The full-root scan cost is a
pinned-build characteristic to revisit when OELS gains configurable
resource-walk pruning upstream.
## What is deliberately not changed
- No `metadata.yaml`, `.qmd`, `_quarto.yml`, GitHub Actions workflow,
Quarto configuration, dependency manifest, or generated output is
touched by this memo.
- The sibling OELS workspace path used by the current developer for
local testing is intentionally absent from every tracked file. It
appears only inside the developer's own environment.
- No new npm/pnpm/Homebrew dependency is added to the academy; the
reproducible build clones and builds OELS outside this repository.
- No CI gate is introduced. Gating publication on OELS diagnostics is
the responsibility of the separate `Make OELS validation part of
course and academy CI` task.
- The Quarto publishing pipeline, learner-facing behavior, and existing
course/lab verifiers are unchanged.