Repository navigation
docs: how to expose a programmatic API, and the three gaps writing it found - #41
Conversation
A plugin can offer an application values of its own, and it always
could: §11's exports are read back with `host.exports('<ref>/<key>')`,
a definition may publish as many keys as it likes, and the unqualified
alias lets a library ship a one-line accessor that works whether the
instance is tagged or not. That was written down for plugin-to-plugin
use only, so the question arrived from sekreto as though the mechanism
were missing.
DOCS.md §2 now carries "Expose a programmatic API": the two calls, the
several-exports shape, the alias rule, exports of a `loaded` instance,
and when to export versus provide a capability. No library change.
Writing it surfaced a corpus coverage gap, recorded as register row 6.7
rather than closed here. Every `export` entry reads the key `client`:
the `probe` definition exports two keys but nothing reads the second, so
a port whose `export` overwrote rather than keyed could stay green
depending on write order. Closing it needs `probe` to export a second
SCALAR key, which is a probe-catalog change and so a 23-port change set.
No port is known to be wrong; same class as gaps 2 and 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Three errors, all in the new how-to: a missing Oxford comma, and two `above`s where the house rule wants `preceding`. The second one also read better as a named section than as a direction. `make scan-prose` says out loud that it is skipping the Vale half when vale is not installed. Running `tools/check_prose.py` directly, as this branch did, does not -- it is only ever the second half of the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
|
| version | |
|---|---|
lean/lean-toolchain |
leanprover/lean4:v4.32.1 |
.github/workflows/ci.yml |
v4.15.0 |
48392f5 moved the port to 4.32.1 for the reason its message gives — four
repositories are vendored together as one checkpoint and cannot be
compiled side by side on three different pins. The workflow still fetches
the 4.15.0 release asset, so CI builds 4.32.1 sources with a toolchain
that predates the String.Slice changes that commit was written for.
doc/plan/status.md still names 4.15.0 as the pinned asset, for the same
reason.
Why the workflow was not moved with it: an agent session cannot write
.github/workflows/. Both write paths refuse the path — git push rejects
the ref without workflow scope, and the App's file API fails on that path
alone. That is stated in AGENTS.md §5b, and it is why the port's bump
landed without its workflow counterpart.
The fix, for someone who can push a workflow:
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
- if: matrix.port == 'lean'
run: |
curl -fsSL -o /tmp/lean.tar.zst \
- https://github.com/leanprover/lean4/releases/download/v4.15.0/lean-4.15.0-linux.tar.zst
+ https://github.com/leanprover/lean4/releases/download/v4.32.1/lean-4.32.1-linux.tar.zstTwo lines nearby also name the old version and should move with it: the
comment above the step, and the lean 4.15.0 from a pinned GitHub *release asset* note in doc/plan/status.md.
I have not opened a patch/ folder for it. §5b's convention is that a
workflow change arrives as its own delivery, and putting an unrelated one
on a documentation PR would widen this PR rather than deliver that fix.
It wants its own branch.
No re-run: the mismatch is deterministic, so a second attempt fetches the
same 4.15.0 asset and fails the same way. Nothing here blocks this PR's
own content, and the prose gate — which was this PR's, and was real — is
fixed in b111106.
Generated by Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fbc1638363
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex's review, verified against the canonical. Five of its six findings
were real; each is fixed here, and the two that turned out to be
library-sized are register rows rather than edits.
Corrected in DOCS.md:
- A CAPABILITY IS NOT AN ALTERNATIVE TO AN EXPORT, and the text said it
was. `inst.provides` records selection metadata and
`host.capability(name)` answers with provider refs, so neither carries
a callable value: the capability is the edge the host resolves and
orders, and the API still travels as an export or a binding. A
provider following the old sentence would have left its consumer with
a name and nothing to call.
- The unqualified alias filters by KEY before tag. `resolveexport`
builds its candidate list from entries matching the key, so an
untagged instance that exports something else does not shadow the
tagged one that exports this. Reproduced both ways.
- `{ read: store.read }` loses its receiver. A method reading `this`
breaks the moment it is lifted off the object and the host calls it;
the example is a closure now, and says why.
- The spec splits at the FIRST `/`, so the `<ref>/<key>` form addresses
only a name without one. Stated, with the register row.
- `export/key#loaded-visible` named as what pins loaded-export
visibility.
And `doc/plan/`:
- 6.8: a scoped definition (`@acme/store`, legal under §4 and handled by
`resolvecandidates`) has no export spelling. `@acme/store/admin` reads
as ref `@acme`, key `store/admin`, and answers undefined. No `export`
entry covers it — all of them use `probe`, which has no slash.
- 6.9: §11.1 says `inst.capability(name)` returns the bound provider.
The canonical instance api has no such method. A design statement no
corpus entry can distinguish, which is handover.md §13's first kind.
- The snapshot date, which said 2026-09-04 while this change added to it.
The sixth finding cited a rule that does not exist: AGENTS.md §5c is the
prose gate and says nothing about behaviour statements naming their
corpus entry. The citation is added anyway, on its merits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
`lean` has failed on every CI run since 48392f5, including every run on `main`. That commit moved the port to Lean v4.32.1; `.github/workflows/ ci.yml` still fetches v4.15.0, so CI compiles 4.32.1 sources with a compiler predating the `String.Slice` changes the port was rewritten for. It fails at `make build-lean`, which reads as a port failure and is not one. REPRODUCED BOTH WAYS rather than inferred. Both compilers were fetched and the port built under each: v4.15.0 Some required builds logged failures: - Corpus - Plugin.Env - Plugin.Version v4.32.1 clean; 576 corpus entries across 19 sections, all pass The port's bump landed without the workflow because an agent session cannot write `.github/workflows/` -- the boundary AGENTS.md section 5b names, and the reason this folder exists. The patch adds a comment above the step saying the two versions move together, so the next bump has one place to look. `patch/` means work applied by nobody yet: apply it and delete the folder. Two things it needed beside itself. `tools/check_prose.py` called `patch/` a twenty-fourth port, because any top-level directory with a README.md is one and `patch/` predates the gate -- its README is a working document addressed to whoever applies it, so `NOT_PORTS` now carries it. And `doc/plan/status.md` still advised lean 4.15.0 as the pinned asset, under a heading about adding a new port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
Register row 6.8, found by Codex's review and reproduced against the
canonical: `resolveexport('@acme/store/admin', ...)` answered `undefined`.
Section 4's `checkname` permits `/` in a NAME -- `@acme/store` is legal,
and section 10.2 resolves a scoped name verbatim -- so splitting at the
FIRST `/` read that as the ref `@acme` with the key `store/admin`, which
matches nothing. A scoped definition had no spelling for its exports at
all. Every `export` entry in the corpus used `probe`, which has no
slash, so nothing saw it.
THE COST IS STATED RATHER THAN IMPLIED: an export key may not contain
`/`. One separator cannot serve both. A name holds slashes because
section 4 says so; a key held them only because nothing stopped it, and
nothing in this repository or in sekreto declares one.
Four corpus entries, all in `export/key`: a scoped ref qualified, the
same untagged, the alias half (which has to split the same way or a
scoped name resolves qualified and not unqualified), and a scoped miss
still answering absent. Reverting the canonical to `indexOf` fails
`export/key` and no other group.
Canonical plus all 22 ports. Four had no reverse search and take a
workaround the comment names: lua walks forward keeping the last hit,
elixir takes `List.last(:binary.matches ...)`, haskell and lean split
the REVERSED string and put the halves back the right way round.
Design section 11 and DOCS.md now state the split and its cost. The
design's alias sentence was corrected in the same pass: the key filters
candidates before the tag does, so an untagged instance exporting
something else does not shadow the tagged one exporting this.
Corpus 572 -> 576. All 23 ports build and pass all 576.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
Register row 6.7. `probe` exports `mark`, the constant `marked`, beside
`client` and `inst`, and four entries read a second key off one
instance: `export/key#second-key`, `#two-keys`, `#two-keys-reversed` and
`export/alias#second-key`.
THE GAP AS FIRST WRITTEN WAS OVERSTATED, and measuring it is what said
so. The claim was that a port whose `export` overwrote rather than keyed
could stay green. It could not: `probe` writes `client` before `inst`,
so an overwriting `export` hands an instance api back for `client` and
fails outright, and a lookup that IGNORES the key is caught by the
absent-key entry `probe$a/nope`, because ignoring the key makes a
missing key present.
What nothing could distinguish is narrower and more plausible: a lookup
that CHECKS the key is present and then answers with the instance's
FIRST export. Presence-checked, position-answered. Mutated both branches
that way and measured against both corpora:
exact branch, 572 entries export/key and export/alias GREEN
(featurehost-bridge and resource/scope
fail, but only because the DRIVER reads
exports(ref + '/inst') for `stray` --
not an entry asserting anything)
exact branch, 580 entries export/key fails
alias branch, 572 entries the WHOLE suite green, 0 failures
alias branch, 580 entries export/alias fails, and nothing else
So a port could have got exports wrong and been told by a resource test,
which is the wrong place to learn it, or not told at all.
A CONSTANT rather than the tag or the name, because it is the same
literal in twenty-three languages and needs no accessor; `inst` cannot
close the gap because it is an instance api, shaped differently in every
port, and no entry can assert on it.
23 drivers, DOCS.md section 4.3's probe table. Corpus 576 -> 580, all 23
ports build and pass all 580.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
Register row 6.9, the last of the three gaps the export how-to found. §11.1 said "the first is bound, and `inst.capability(name)` returns it" and the instance api had no such method: `bind`, `export`, `provides`, `acquire`, `release`, `position`, `nest`, and no `capability`. Implemented rather than struck, because `host.capability` cannot answer it. That one RANKS the live providers; this one answers with the provider THIS instance took, and §11.4's reluctant rebinding makes the two different refs at the same instant. It reaches `chosen` with `remember` true — the instance is asking, not introspection about it — and answers with a ref, so the API still travels as an export. A corpus entry reaches it through the `dep` probe's new `options.capof`, which names one of the instance's own requirements and exports the answer as `cap`, from `activate` because that is when the selection is made. Nothing else can: an instance api is shaped differently in every port and no entry can assert on one. The first four entries did not earn the method, and mutating it is what said so. Against the selected one, rank deciding it, an optional requirement nobody provides and a name this instance does not require, `host.capability(name)[0]` — the exact thing the method exists to not be — passed all 584, and so did `requirements[0]`, which never reads the name. Six more entries close both. `#by-ref` turns on §11.1's "a ref satisfies too": a ref is not a declared capability, so the instance answers `dep$p` where the host ranks nothing, and `#by-ref-host-disagrees` is the same run asking the host. `#named-first` and `#named-second` needed an instance with TWO satisfied requirements, which no other entry here has. `#reluctant` and `#reluctant-host-ranks` record the divergence the design claims and nothing else stated. `remember` true rather than false stays unpinnable: activation's own remember-all loop runs immediately after the callback that asks. Canonical plus all 22 ports, `c` taking a forward declaration because `chosen` is defined below it. Corpus 584 -> 590, all 23 ports green. Design §11.1 and §11.3 now describe something that exists; DOCS.md §2 gains the consuming half of the export how-to and §4.3 documents `capof`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
The canonical `dep` acquires one synthetic resource per activation, the way `probe` does. `c`, `cpp`, `ocaml`, `zig` and `lean` gave it no `activate` at all, so in five of twenty-three ports it held nothing. The corpus could not tell. `open` is in every entry's `out`, and no entry with a live `dep` had ever asserted it — so the probe catalog, which DOCS.md §4.3 exists to keep identical, diverged in silence. Found while giving `dep` an `activate` for `options.capof`, by reading the canonical rather than the nearest port. `depend/bound#dep-holds` asserts `open: 2` for a provider and a consumer. Taking the `acquire` back out of any one of the five fails that entry and nothing else. Corpus 590 -> 591, all 23 ports green, and §4.3 now says `dep` holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
Correction to the
|
`lean/lean-toolchain` moved to v4.32.1 in 48392f5, for the reason that commit gives: voxgig/struct's lean port and the sdkgen scaffold both pin it, and four repositories vendored together as one checkpoint cannot be compiled side by side on three different pins. This file still fetched v4.15.0, so CI compiled 4.32.1 sources with a compiler that predates the `String.Slice` changes 48392f5 was written for. `make build-lean` has failed on every run since, on `main` as well as on every branch, and it reads as a port failure rather than a toolchain mismatch: the three modules that fail are `Corpus`, `Plugin.Env` and `Plugin.Version`, on `String.dropEnd` and `String.toString` not existing. The comment now says the two must move together, so the next bump has one place to look. `doc/plan/progress.md` named the old asset and moves with it. `patch/` is gone, which is what applied looks like, and `tools/check_prose.py` drops the `patch` entry it needed while the folder was there. Verified: the v4.32.1 linux asset resolves at the URL this builds, and the port passes all 591 corpus entries under it locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8
A plugin can offer an application values of its own, and it always
could. §11's exports are read back with
host.exports('<ref>/<key>'), adefinition may publish as many keys as it likes, and the unqualified
alias lets a library ship a one-line accessor that works whether the
instance is tagged or not.
That was written down for plugin-to-plugin use only. The
application-facing half appears in the design and in the reference table,
and nowhere as a how-to — so the question arrived from
sekreto as though the mechanism were
missing. It is not, and the answer to it needed no code.
Writing the how-to turned up three gaps, though, and this PR closes all
three, plus a fourth found while closing them. The corpus goes 572 -> 591
and every one of the 23 ports is green on it.
The how-to
DOCS.md§2 gains Expose a programmatic API, between Depend onsomething and Hold a resource:
inst.export(key, value)indefine,host.exports('<ref>/<key>')toread it back
shape is two: one the host or its framework consumes, one the
application does
raises rather than picking one
loadedinstance are visible (export/key#loaded-visiblepins it), and the getter convention for a value that only means
something while live
inst.providesrecords selection metadata andhost.capability(name)answers with refs, so neither carries a callable value
inst.capability(name)for the provider thisinstance took, paired with
host.exports(ref + '/key')to reach itThe worked example is sekreto's mini vault
(voxgig/sekreto#31), which
exports
providerfor the chain andvaultfor the application from onedefine.6.7 — every
exportentry read the keyclientSeven reads of one key, plus one absent key.
probeexported two keys(
client, and its owninst), and nothing read the second — butinstis an instance api, shaped differently in every port, so no entry can
assert on it.
probenow exportsmark, the constantmarked, and fourentries read a second key.
The gap as first written was overstated, and measuring it is what said
so. A port that IGNORED the key outright was already caught, by the
absent-key entry: ignoring the key makes a missing key present. What
nothing could distinguish is a lookup that checks the key is present and
then answers with the instance's FIRST export. Mutated that way, the
alias branch left the whole 572-entry suite green — 0 failures. Against
the new entries each mutation is caught by its own group.
6.8 — a scoped definition had no export spelling
resolveexportsplit at the first/, so@acme/store/adminread asthe ref
@acmewith the keystore/adminand answeredundefined. §4'schecknamepermits/in a name andresolvecandidatesresolves ascoped name verbatim, so scoped definitions were a supported case with no
way to read their exports. No
exportentry could see it: all of themuse
probe, which has no slash.Closed by splitting at the last
/, canonical plus all 22 ports,with four new
export/keyentries. The cost is stated in the design andin
DOCS.md: an export key may not contain/, because one separatorcannot serve both and a name holds slashes because §4 says so. Four ports
have no reverse search and take a documented workaround. Reverting the
canonical to
indexOffailsexport/keyand nothing else.6.9 — §11.1's
inst.capability(name)did not existThe design said "the first is bound, and
inst.capability(name)returnsit", and the instance api had no such method. Implemented rather than
struck, because
host.capabilitycannot answer it: that one RANKS thelive providers, this one answers with the provider this instance took,
and §11.4's reluctant rebinding makes the two different refs at the same
instant. It answers with a ref, so the API still travels as an export.
A corpus entry reaches it through the
depprobe's newoptions.capof,which names one of the instance's own requirements and exports the answer
as
cap.The first four entries did not earn the method, and mutating it is what
said so. Against them,
host.capability(name)[0]— the exact thing themethod exists to not be — passed all 584, and so did
requirements[0],which never reads the name. Six more entries close both:
#by-refturnson §11.1's "a ref satisfies too" (a ref is not a declared capability, so
the instance answers where the host ranks nothing),
#named-firstand#named-secondneed an instance with two satisfied requirements, and#reluctantplus#reluctant-host-ranksrecord the divergence thedesign claims and nothing else stated.
remembertrue rather than falsestays unpinnable, and the register says so.
A fourth, found while closing the third
Giving
depanactivatemeant reading the canonical's, which acquiresone synthetic resource per activation.
c,cpp,ocaml,zigandleangavedepnoactivateat all — five of twenty-three ports whereit held nothing. The corpus could not tell:
openis in every entry'soutand no entry with a livedephad ever asserted it, so the probecatalog diverged in silence. Fixed in all five, pinned by
#dep-holds,and
DOCS.md§4.3 now saysdepholds. Taking theacquireback out ofany one of them fails that entry and nothing else.
The
leanCI job, fixed hereleanwas red on this branch and red onmain: the port pins Leanv4.32.1 and
ci.ymlstill fetched v4.15.0, so CI compiled the port'ssources with a compiler that predates the
String.Slicechanges48392f5was written for. The three modules that failed —Corpus,Plugin.Env,Plugin.Version— are files this branch never touches.a38ab7dbumps the asset to v4.32.1 and adds the comment saying the twopins move together.
leanis green on this branch now, and so arethe other 27 checks.
An earlier comment on this PR said an agent session cannot write
.github/workflows/and delivered the change as apatch/folderinstead. The push went through this time, so the folder is gone — which
is what applied looks like — and
tools/check_prose.pydrops thepatchentry it needed while it was there.The review round
Codex found six things. Five were real and are fixed in
5c1df34: thecapability sentence above, the alias being key-specific, a
{ read: store.read }example that loses its receiver, the first-/limit, andthe
export/key#loaded-visiblecitation. The sixth cited a rule thatdoes not exist —
AGENTS.mdL247-255 is §5c, the prose gate, and saysnothing about behaviour statements naming their corpus entry. The change
was made anyway, on its merits, and the reply says the citation was
false. 6.8 and 6.9 both came out of that review.
Checks
All 28 CI checks green. Locally
make checkexits 0: all 23 ports pass591 entries, and
make spec-check,make parity,make probes,make versionsandmake scan-prose(vale 3.14.0 + check_prose, 19 pages) allpass. Every toolchain was installed and run locally rather than asserted.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CVMgjjwQfTJ2rXz637Wpk8