FAQ and troubleshooting
The framework catches many defects at compile time, but not all problems emit compiler errors. This page is grounded in actual failure modes encountered during development, with the source files you can read to understand the root cause and how the gates prevent regress.
I hit "undefined symbol" from the linker, but lake build <module> succeeds
Root cause: After a module gains or moves definitions, incremental builds can fail with a linker error that names a module but points nowhere in its source.
ld.lld: error: undefined symbol: l_Super_App_app
Some required builds logged failures: - Super.Screens:c.o
The module's source is fine. The issue is stale object files left by the incremental build system.
Solution: Run npm run build — build.mjs detects whether Lean reported an actual source error (file and line) or a phantom linker error. If it's phantom, it runs lake clean and retries. See /home/basque/dev/impl-super-encapp/build.mjs for the detection logic (it reads both stdout and stderr looking for \.lean:\d+:\d+:).
Why this happens: Lean's lake build system records module dependencies based on the definitions it sees at build time. When you add a definition to a module or move it, the .o files downstream can reference a symbol that no longer exists at the old address, even though lake build <that one module> recompiles it correctly. Running the full build (lake exe super) links against stale .o files from upstream modules.
Silently cleaning would hide real compile errors (making a typo take four minutes to see), so the build script prints real errors and exits non-zero, only cleaning on phantom linker errors.
My proof passes but the shipped app doesn't work — it's using old code
Root cause: There are two independent staleness holes:
-
Adapter staleness: You edit
adapter/enc-entry.mjs, run the test suite, and it all goes green. But the shipped HTML (dist/index-live.html) still<script src>s the olddist/adapter.bundle.js. -
Binding staleness: You edit
Super/App.lean, run the probe tests, and they all pass. But they're asserting against the OLD declared binding read from a staledist/index.html.
Both are file-timestamp-detectable.
Solution: Before running tests or probes, run:
npm run build # regenerates index.html + adapter.bundle.js
node test/bundle-fresh-check.mjs # fails if artifacts are stale relative to sourceThe check script is in /home/basque/dev/impl-super-encapp/test/bundle-fresh-check.mjs. It compares mtime of:
Super/App.leanvsdist/index.html- Every file in
adapter/vsdist/adapter.bundle.js
Why this exists: Probes and tests import adapter/enc-entry.mjs (the source), but the app loads the emitted bundle. A divergence between the two is invisible to the test suite because the suite exercises the source adapter directly. The replayers load the bundle, so they see the old code.
My workflow proof holds, but it fails when I replay it
Root cause: The proof tier and the execution tier are two different claims, both necessary but not identical:
- Proof:
Workflow.holds app w = truebynative_decide— the Lean interpreter and the app logic agree on the post-conditions. - Execution: The same workflow replayed on real UI runtimes (happy-dom, headless Chromium, Electron) — the JS runtime and the Lean interpreter agree empirically.
The proof says nothing about whether the emitted JavaScript matches the Lean interpreter. That match is checked empirically, not proven.
What it means: If a proof passes but the replayer fails, the likely causes are:
-
JS runtime divergence — the emitted code does something the Lean interpreter doesn't, or vice versa. See
/home/basque/dev/impl-encapp/test/fuzz-parity.mjsfor differential fuzzing across runtimes (seeded regression tests that each caught a distinct divergence class). -
Adapter gap — the adapter implements the store seam but is hand-written. If the adapter violates the
HostBindingprotocol (e.g., doesn't callonChangewhen the app mutates the store), the proof can pass while the replayer sees stale state. See Host bindings for how to move policy out of the adapter and into declared bindings. -
Multi-user or persistence gap — the proof tier is single-
Doclogic (one app instance, no cross-user state, no reload). If your workflow requires two app instances or multi-enclave sync, the proof is out of scope. That story lives in the node layer. See What is proven for the honest boundary.
How to debug: Run the same workflow on all four replayers and see which ones fail:
npm run replay:dom # happy-dom, fastest, no real browser
npm run replay:hl # headless Chromium, real browser engine
npm run replay:rn # React Native (native simulator if available)
npm run replay:e2e # Electron, if in CI environmentIf only the headless Chromium run fails, the bug is probably in the JS emitter or a browser-specific edge case. If the dom replayer passes but Electron fails, it's likely native code or a native binding issue.
The replayer crashes with "no such file" for webkit libraries
Root cause: Playwright (the replayer's browser driver) tries to launch Webkit (Safari's engine) but the system does not have the required host libraries installed.
Error: browser launch failed
... missing libgstreamer...
Solution: Use chromium or firefox instead:
npm run replay:hl -- --browser chromium # works out of the box
npm run replay:hl -- --browser firefox # also works, usually
# npm run replay:hl -- --browser webkit # requires webkit development headersThe replayer defaults to chromium. See test/encapp-replay.mjs for the Playwright configuration.
Why this matters: Webkit (Safari's engine) requires system development libraries (libgstreamer-plugins-base, libgstreamer, libharfbuzz, etc.) that are not present on all CI runners. Chromium and Firefox ship as portable binaries and work out of the box. If you need Webkit coverage, you must provision those libraries on the runner or use a base image that includes them.
My axioms look wrong in the build output, or I see sorryAx
Root cause: lake build allows sorry in proofs (it's only a warning), so a keystone proof could rot to unsound and still build green. Keystones like Foundation.Layer.mono are the foundation of the whole framework, so rotting one is a showstopper.
Solution: The soundness gate runs first in the test suite:
node test/axiom-audit.mjs . Encapp.Core Foundation.Layer.mono ''This fails immediately if:
- The module contains
sorryAx(an unsound axiom) - The module uses an axiom outside the accepted set (pinned per module)
It must run before any proof is trusted. See /home/basque/dev/impl-encapp/test/README.md under "Soundness gate" for the full inventory.
Why it's automated: The audit uses #print axioms to extract the exact axiom set at build time, so if a maintainer accidentally adds an axiom without updating the gate, the test fails. You cannot commit a gate-breaking change.
Foundation is in the parent directory, but lake can't find it
Root cause: The foundation library (Enc.Foundation or similar) must be a sibling checkout — at the same depth as impl-encapp, not nested inside it.
If your directory structure is:
my-project/
impl-encapp/ ← you are here
Then foundation is not accessible.
Correct structure:
my-workspace/
foundation/ ← must be here, sibling to impl-encapp
impl-encapp/ ← you are here
impl-super-encapp/ ← optional, also works as sibling
Solution: Check your lakefile.lean. It should declare the dependency with a relative path:
require foundation from "../foundation"If foundation is not checked out as a sibling, clone it:
cd ../
git clone <foundation-repo>
cd impl-encapp
lake build # should find ../foundation nowWhy this matters: lake resolves relative paths from the workspace root. If you nest foundation inside impl-encapp or put it elsewhere, lake cannot find it and the build fails with a cryptic "package not found" error.
How do I run the whole verification suite?
Command:
npm run verify # lint + smoke + units + web build + bundle-size budgetEquivalently, step by step:
npx eslint . # lint (must pass with --max-warnings 25)
npm run test # smoke + algebra + units (27 + 25 = 52 tests)
npm run build # Vite production build
node test/bundle-fresh-check.mjs # bundle artifacts vs sources (mtime check)The most useful subset for rapid iteration:
npm run build # ~1s, catches app.lean + adapter drift
npm run test:workflows # ~8s, proves + replays the whole corpusIf anything fails, stop and fix it before committing. The test invariants (27 algebra checks + 25 unit tests passing, both bundles compiling, no axiom rot) are gates on every commit.
Recap
- Stale links:
lake cleanbefore retrying if a linker error disappears after clean. - Stale bundles: Run
npm run buildbefore testing;bundle-fresh-check.mjscatches divergence. - Proof vs runtime: Proofs are Lean theorems; runtime matching is empirical (four independent replayers).
- Single-
Docboundary: Proofs do not cover multi-user, persistence, or cross-enclave logic. - Axiom rot:
axiom-audit.mjsmust pass;sorryis permitted but audited. - Foundation location: Must be a sibling checkout, not nested.
- Webkit failures: Use chromium or firefox if webkit libraries are unavailable.
Next steps
- Read What is proven for the formal boundary and the theorems you get.
- See Host bindings if your adapter needs to expose store policy.
- Check test/README.md for the full gate hierarchy (proof tier, coverage gates, axiom audit, native parity).