Repository navigation
♻️ Share one <Git.Push /> and <Issue> between the REPL and every other command - #951
Conversation
`<Git.Push />`, `<PullRequest>`, its three evidence reads and `<Issue>` had two
implementations each: the bundled one every document command registered, and a
second authored body a REPL entry installed over it with its own identity
behind it. Typing the same element meant different code depending on which host
was running, which is the boundary violation this repairs.
Each of those six now has one implementation, in the provider-neutral half of
`@executablemd/git`. A body interprets its props and forms, expands its own
content, normalizes what the document wrote, asks a semantic feature
operation — `GitApi["push"]`, `IssueOperationsApi` or
`PullRequestOperationsApi` — and projects what came back. None of them receives
an authenticator, an owner, a site, native access, a policy object, a record
codec or any publication state, and none names a journal record.
Between the component and the host is one feature-owned binding. For a host
that retains work named after an invocation it authenticates the invocation
canonical core issued — once, in the engine frame the component was entered in,
before any content is projected — then builds that invocation's own operation
facade from the host's policy and runs the one body against it. Revocation is
armed before the facade can be used and runs on success, failure and
cancellation, so a facade kept past its invocation performs nothing, and one
authored element performs one external operation.
A host that retains nothing named after an invocation registers those six
ordinarily over the same bodies with no identity authenticated, because there
is nothing for one to name — so they stay ordinary defaults a nearer
registration shadows. A `repl` command registers the other six and receives
these as declarations carrying the entry's policy, so no executable default is
registered for a declaration to shadow. `deno/retained-repository/{push,issues}`
are policy now: `replPushPolicy` and `replIssuePolicy` replace
`replPushComponent` and `ownedIssueComponent`, and the retention, dispatch,
reconciliation, envelope and transport beneath them are unchanged.
Retained-entry `<PullRequest>` stays unavailable: its declaration uses the
common construction with the ordinary policy, which is its own
missing-provider refusal.
Nothing observable moves. The owner is still
`canonicalJson(["executablemd.repl.issue-owner",1,execution.id,entryKey])`, the
site is still the authenticated `expansion.id`, and the Push v1 request, its
digest domain, `git_host_effect`, `issue_effect`, the fingerprints, the
idempotency keys, the coroutine order and `COMPOSITION_ORIGIN`'s bytes are
untouched. There is no migration, alias or journal rewrite.
Seven histories the merged implementation wrote are the evidence for that.
`packages/git/tests/fixtures/retained-repository-history/` holds their exact
sources and journal bytes, captured from old main
`7ee526665dcadd9ec79dca9f8b6a4726903d9a4f` before production was edited, with a
receipt naming the revision, the command, the owner, the paths each record
addresses, where each history was cut and what service state the capture left
behind. Each is cut while its root is unfinished, so continuing one re-enters
the component rather than replaying a settled root, and the new suite continues
all seven through the shared components without regenerating a byte.
Three gaps in delivery A's historical evidence, found in review of `6166fffc189585779c2536a49477de825e630d5e`. **The journals were not the bytes the capture emitted.** `deno task fmt` runs `oxfmt --write packages scripts`, and oxfmt formats JSON, so the pass that followed the capture reserialized five of the seven committed journals. Their parsed values never changed, but the bytes are the evidence: A3 preserves the old representation, not an equal reading of it. Each journal is restored with the capture harness's own serializer, which reproduces the SHA-256 its receipt already recorded, and the fixtures directory is excluded from oxfmt so a later format pass cannot rewrite them again. The provenance row now verifies all fourteen file hashes against that receipt, so this drift fails here rather than in a reviewer's audit. **The continuation depended on the capture machine and wrote shared paths.** It wrote each source back to a fixed capture path with no cleanup and ran the suffix against the real host filesystem there. Now every invocation owns everything physical it needs: the root source is supplied by value at the path its own record holds, so nothing is written to disk; the document filesystem is this invocation's own, keyed by the recorded logical paths, so the retained middleware records what it always recorded and two continuations cannot see each other's work; and the component search path is an owned empty directory rather than the capture's, which is what made an `<Elicit>` import read a directory only this machine has. A new row continues two histories at once and shows their written paths are disjoint. With every capture directory denied for the four issue and failure fixtures, and the clone, bare origin, seed and entry document denied for the three publication fixtures, all nine rows pass. What remains is one directory: the managed placement's own durable location. Denying just that fails exactly the three rows whose continuation performs new covered file work or a pending publication, because production takes that placement's slot lock there and proves that checkout's object database there, through `@effectionx/fs`, `node:fs` and the advisory-lock file rather than through a contextual Api. That consumer is stated in the suite's own prose and returned for architecture resolution rather than worked around with a new production seam. **H5 compared the inherited records with themselves.** The old source binds `filed` and `found` and consumes neither, so the row read the retained events out of the output stream and compared them with those same events. It now states what the components must restore — the upsert's URL, and the complete answer the public middleware composed for the read — as values of its own, parses the restored records through the production parsers and asserts against those, so a schema-valid value that is not this one fails. Both public Issue chains are installed to fail if entered, and the row shows neither is.
The accepted A3 amendment narrows the historical continuation contract. H1, H2 and H3 — a completed publication, a pending one, and one the remote accepted before the journal could record it — are no longer continued here. Continuing any of them performs new covered file work or resumes a pending publication, which takes the recorded placement's slot lock and proves that checkout's object database at the absolute paths the journal holds. Both belong to the real native host and stay there, so no invocation-owned substitute reaches them, and the proposed native-host seam was withdrawn rather than built. Their three cases and the helpers only they used are deleted rather than skipped: a skipped test reads as a gate somebody means to reopen, and this is a decision instead. Their fixtures, hashes, receipt and the capture harness are untouched, so a later continuation has unchanged bytes to work from. What remains is six steps. All seven captured histories are still held to the fourteen source and journal hashes their receipt records, to their event counts, to their unfinished roots and to their recorded owners. Four are continued: the definitive publication failure restores provider-free; the completed Issue read and upsert restore their exact composed answer and URL with neither public chain entered; the unacknowledged upsert presents its original key and adopts the issue already at the tracker without a second create; the definitive Issue failure restores without a transport. Two continuations still run at once and own separate state. The suite now says what it does not prove. It does not show that the three old publication histories continue through the extraction, and it names the current-code replay and reconciliation evidence that remains required without standing in for that.
A REPL entry no longer carries a component catalog of its own. The Git Plugin registers all twelve names for every command it declares for, `repl` included, and that registry is what says which names exist: an execution that retains one of the external operations declares that same name again in its own scope with its own policy under it, where a nearer declaration shadows the registered default. So the entry declares what it owns and nothing more — `<Git.Push />` and `<Issue>` for the repository installation, `<Issue>` alone for the issue-only one. `<PullRequest>` and its three evidence reads keep the registered defaults and their own missing-provider refusals, until the delivery that retains them owns their policy too. Gone with it: `entryComponents` on the REPL profile and session, the preflight parameter and component merge, the no-host fallback that turned that catalog into an installation, `profileComponents`, `CommandPlugins.components`, the `plugins.components` reads in Syntax and Plan validation, the Plugin's `RETAINING_COMMANDS` vocabulary split and its root export, and the bulk six-operation policy and declaration catalog the split needed. Syntax inspection, Plan validation and REPL preflight read the installed registry they already stood in.
The five modules the component boundary changed had grown a comment above almost every declaration, a module opening that reproduced the specification and an architecture paragraph at each constructor. Repeating the canonical form-dispatch rule four times per file made the one constraint that matters harder to find, not stronger. What stays is what a routine-looking refactor would break: authenticating the invocation in the engine's own frame before the body projects content, arming revocation before the facade can be used, finishing child content before the parent operation or retained envelope, and the pinned initial branch a base falls back to. Each form-dispatch boundary explains itself once, where the dispatch is. Comments only, with one exception: `FacadeRefusal`'s two members no longer carry a doc comment each, so the union fits on one line. Same type, same members. Every `documented()` value, description, prop schema, return schema and refusal sentence is untouched — those are program data that Syntax and the documentation read, not commentary.
The binding had grown a facade framework around the one thing it is for. It built a gate per invocation, tracked `open` and `used` on it, armed an `ensure` to close it, and refused a late or second call with its own error type — and every ordinary registration, which retains nothing and has no identity to name, was wrapped in the same machinery. None of that was guarding a real escape. An operation runs in the scope of the invocation that asked for it, and Effection already ends it there; the single-use rule was marked before the work ran, so it never said an external effect had been acknowledged. What it did do was make the interesting line — authenticate the invocation core issued, in the engine's own frame, before the body projects content — one of thirty. So the binding is now that line plus the handoff: authenticate, ask the feature-owned policy for this invocation's operations, run the shared body. Ordinary registrations call the public feature operations directly, through a three-line adapter that supplies the body's second argument. Gone: `InvocationGate`, `FacadeRefusal`, `BoundOperationError`, `performed()`, the `open`/`used` state, `unboundComponent` and the four retired exports. The two rows that manufactured a saved facade to watch it refuse are gone with them — they established the guard's implementation, not a production escape. The cancellation row keeps the part that is about the product: the halt stops the suspended upsert, joins it, and nothing performs twice. Real retention, delegation, reconciliation and native cancellation stay where they are proved, in the owned-worker and native suites. Owners, sites, record bytes, keys, fingerprints and coroutine positions are unchanged, and so is every `documented()` value, schema and refusal sentence.
Four of the six external operations were being handed an operations object that was only ever the feature Api's own. `ORDINARY` built forwarding arrows for it, `registeredComponent` adapted the body to it, and `PullRequest` and its three evidence reads took it as a parameter — all so that a host could substitute policy underneath. No host does: A retains publication and filing, and nothing else. So those four now ask their Api the way every other component in the repository does. `pullRequestRegistration` is a value rather than a constructor, `pullRequestReadRegistration` takes the kind alone, their bodies are module-private again, and `ORDINARY` and `registeredComponent` are gone — `Git.operations.push` and `IssueOperations.operations` are passed where the registration still needs one, because a contextual Api's operations resolve their chain from the scope at call time, not from where the reference was taken. `<Git.Push />` and `<Issue>` keep the argument, and the reason is the one thing worth a comment: a REPL entry's durable boundary has to *enclose* the public chain, and a body that reached the Api itself could not be enclosed by anything above it. That is also why neither retained policy can move to the Api seam — `retained-repository-push.test.ts` P18 proves a document's own component calling `Git.operations.push(...)` inside an entry reaches the ordinary missing-provider refusal and retains nothing, and `repl-issues.test.ts` proves a public handler that answers a read is retained as the entry's answer while a completed replay enters no handler at all. Installing either lifecycle as Api middleware inverts both. Retained Issue construction is consolidated into the envelope the two profiles already shared. `issue/owned.ts` now owns the live transport call and the idempotency-key derivation — identical in both profiles all along — so `effect.ts` calls them directly beneath the public chain, the entry composes the chain over them, and `transportRead`/`transportUpsert`, their duplicate parses and the entry's own key derivation are deleted. What is left of `replIssuePolicy` is the owner it closes over, the authenticated site it is handed, and which side of the public chain its envelope sits on. `replPushPolicy` keeps its ten lines for the same reason, and `publish()` beneath it stays entry-specific: it resolves this entry's placement, slot and saved request, none of which a workflow run has.
…oes (#181, A) A workflow run does retain `<PullRequest>` and its three evidence reads — the upsert through the Git-host reconciliation beneath it, each read as policy on `PullRequestReadExecution`. Saying they "retain nothing" was wrong about the profile that does. What is true of them is narrower: no host owns a retained boundary *above* their feature Api, so none of the four needs a binding of its own and each asks its Api directly.
The registry is sixteen names now, not twelve, and `<Git.Files>`, `<Git.Changes>`, `<Git.Diff>` and `<Git.File>` join the half a selected checkout answers for. Every count, every catalog sentence and the binding suite's A1 row say sixteen. `publish()` takes the shared `nativeGitTurn` that arrived with those reads, so one entry still serializes its native work through one turn. They also settle the question this delivery kept answering about `<PullRequest>` from the other side: a REPL entry retains every page those four answer with, as `GitQuery` middleware under the component, and declares none of their names. Retaining an operation is not what makes a body take its operation as an argument — owning a boundary that has to enclose the public chain is, and that is still `<Git.Push />` and `<Issue>` alone.
ca5a107 to
218b5e1
Compare
| // The retained-repository history fixtures are journals an older revision | ||
| // emitted, held to the SHA-256 their own receipt records: reserializing one | ||
| // changes the bytes the evidence is, and the suite that continues them | ||
| // verifies those hashes. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // verifies those hashes. |
| title: typeof props.title === "string" ? props.title : "", | ||
| // Verbatim. What a document wrote is the pull request's body, and | ||
| // trimming or reflowing it here would publish something nobody authored. | ||
| // Verbatim: trimming or reflowing it would publish something nobody wrote. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Verbatim: trimming or reflowing it would publish something nobody wrote. |
| yield* local.operations.push({ | ||
| // What a handler answers with is not read as evidence. A synthetic answer | ||
| // has answered the document; it is not a receipt, and this operation | ||
| // retains nothing for it. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // retains nothing for it. |
PR #951: ♻️ Share one
|
Part of #181.
Why
A document writes the operation it wants:
Until now that fragment reached two different implementations.
xmd runreachedthe bundled component. A REPL entry reached a second authored body the retained
provider installed over it, with its own identity behind it — and that body, not the
bundled one, decided what publishing and filing meant. The REPL's needs had been
written into the component instead of underneath it:
What changes
Before:
<Git.Push />and<Issue>each existed twice. The retained entry's copyowned the slot, the lock, the admission and the acknowledgement; the bundled copy owned
the ordinary path. A behavior fixed in one was absent from the other, and the only way
to learn which one a document had reached was to know which command ran it.
After: each operation has one authored body. What changes between an ordinary run
and a retained REPL entry is which policy performs the operation that body asks for,
never the component. Authored behavior, refusals, records and coroutine order are
unchanged in both; every existing history continues to replay, and no key, fingerprint
or journal was rewritten.
How it works
The Git Plugin registers all twelve names for every command it declares for,
replincluded. That registry is what says which names exist — to a run, to
xmd syntax, to<Plan>validation and to a REPL entry's preflight alike. An execution that owns aretained boundary above one of these operations, authenticated per invocation, declares
that same name again from inside its own scope with its own policy under it, through the
ordinary trusted installation and the same component module, where a nearer declaration
shadows the registered default.
Four of the six external operations —
<PullRequest>and its three evidence reads — asktheir feature Api directly, the way every other component in the repository does. A
workflow run does retain all four: the upsert through the Git-host reconciliation beneath
it, and each read as policy on
PullRequestReadExecution. But that retention is installedunder the feature operation rather than owned above it, and no REPL entry owns one
either, so none of the four needs a binding of its own in this delivery. The two that a
REPL entry does retain take the operation as an argument:
boundComponentauthenticates the invocation core issued, once, in the engineframe the component was entered in and before the body projects any content, and hands
that site to feature-owned policy. The site stays private to the policy: no operation
answers with it, no public request carries it and no Context holds it.
That argument is not decoration. An entry's durable boundary has to enclose the public
IssueOperations/Gitchain — a public handler that answers a read is retained as theentry's answer, and a completed replay enters no handler at all — and a body that reached
the Api itself could not be enclosed by anything above it. The same fact is why neither
retained policy can move to the Api seam: a document's own component calling
Git.operations.push(...)inside an entry must reach the ordinary missing-provider refusaland retain nothing.
The two paths are not redundant either. A declaration is registered inside the execution's
own scope, so it cannot be shadowed by an enclosing host's
registerComponents();registration is what makes a bundled component a default a host may replace. That is why
git-push.test.tsandpull-request.test.tsstill replace these components and still seetheir own bodies run, unmodified.
Review guide
Start with:
packages/git/src/composition/invocation-binding.ts— 49 lines, onefunction and two types.
Then review:
packages/git/src/composition/components/— the shared bodies;GitPush.tsandIssue.tscarry a registration and a declaration constructor over one body,PullRequest.tsandPullRequestReads.tscarry a registration alone.packages/git/src/composition/installation.ts— the twelve registrations, inunchanged catalog order, over the feature Apis' own operations.
packages/git/src/deno/retained-repository/— what is left of the entry's oldcomponent bodies:
replPushPolicyandreplIssuePolicy, policy only, holding theslot, lock, admission and acknowledgement they always held.
packages/git/src/issue/owned.ts— the durable envelope, the live transport call andthe idempotency key both retaining profiles share.
packages/cli/src/andpackages/git/src/plugin.ts— how a command's registry reachesits profile, session, Syntax and preflight.
Look carefully at: the ordering in
replIssuePolicy. The envelope is outside thepublic chain and the chain is composed per live attempt, which is the opposite of the
workflow profile's min-band installation. Both orderings are deliberate and both are
tested.
What must stay true
policy that replaced its component, not by the component; a regression that publishes
without a slot or admits twice is caught by the retained Push and Issue suites.
one entry and the site is the authenticated invocation; neither reaches a prop, a
tracker, a Context, a middleware answer or a public request field. A dispatch made
outside the authenticated invocation reaches the ordinary missing-provider refusal.
fingerprint, record shape and coroutine order; checked by continuing four journals
captured from the unchanged merged implementation.
How to verify it
deno task test packages/git packages/cli)prove the shared bodies behave identically under both policies, and fail if the retained
entry stops holding its slot or starts admitting an operation twice.
packages/git/tests/composition-component-binding.test.ts) provesone registry carries all twelve names, that an owned declaration describes the same
component as the registration it shadows, that building a declaration calls no factory,
and that concurrent invocations keep their own authenticated sites.
packages/git/tests/issue-owned.test.ts) proves bothretaining profiles over one envelope, including a workflow fork inheriting an upsert's
identity so it does not file a second issue.
packages/git/tests/retained-repository-history.test.ts) holdsseven journals captured from the merged implementation before any production edit to
their recorded bytes — fourteen hashes against a provenance receipt — and continues four
of them on this code: a failed publication, a completed filing, a failed filing and an
unacknowledged filing adopted without a second POST, two of them concurrently with
independent document state. It fails if this change alters a record, a key or the order
they are written in.
One command for all of it:
deno task testScope
This PR repairs the two operations that are already merged — Push and Issue — and the
installation and command wiring they need.
<PullRequest>and its three evidence readsshare the one-body shape here but gain no entry-owned binding: REPL-owned pull-request work
is an open delivery, and it stays unavailable from a REPL entry exactly as it is today. What
a workflow run retains beneath their Api is untouched.
Converting its construction, and fixing the swallowed pre-attempt authentication refusal it
carries, belongs to that delivery and is deliberately not started here. No migration, alias
or journal rewrite is included, because nothing written by the old implementations needs
one.
New abstractions
invocation-binding.ts(boundComponent, and the two types it is written in) existsbecause
<Git.Push />and<Issue>both need the invocation authenticated in theengine's own frame before their body projects content, and stating that constraint twice
is how one of the two loses it. Two production constructors and two retained policies
consume it.
Generated or mechanical changes
packages/git/tests/fixtures/retained-repository-history/**is captured output, notauthored: fourteen files written by a harness run against the unchanged merged
implementation, plus
receipt.jsonrecording the revision, tree, capture command and theharness's own SHA-256. They are bytes under test and must not be reformatted, which is
why
.oxfmtrc.jsonexcludes them —oxfmtformats JSON.scripts/runtime-test-exclusions.tsmarks the historical suite Deno-only; it drives areal loopback Issue server and native entry policy.
Risks and limitations
7ee52666(delivery 4's merge) and main has since taken ✨ Reuse root JSON across REPL entries and cold reopen (#939) #943 and ✨ Read tracked files, changes, diffs and committed text (#940) #944. Integration is a separate,
already-scoped piece of work that must preserve ✨ Read tracked files, changes, diffs and committed text (#940) #944's
Git.Files/Changes/Diff/Filewith their route and admission handling and theshared
nativeGitTurn, and ✨ Reuse root JSON across REPL entries and cold reopen (#939) #943's root-result and binding ownership. Please review thisdiff on its own base; the integrated revision needs its own review.
Continuing any of them takes the recorded placement's slot lock and proves that
checkout's object database at the journal's absolute paths — both owned by the real
native host, and neither substitutable without a new native seam that was proposed and
deliberately withdrawn. Those three fixtures are still held to their bytes, but their
continuation is not executed, and the suite says so in its own prose. The current-code
replay, reconciliation and containment suites remain required and are not substitutes for
that omitted historical proof.
Scope confirmation