feat(app-boot): register cordis:group beside cordis:include

A composition that shares one `isolate` realm across rows needs a
`cordis:group` row, and a preset living outside this workspace — the
authored ones under the Harness home — cannot resolve
`@cordisjs/plugin-group` by name: Node's upward `node_modules` walk
never reaches the harness from there. Registering it as a loader builtin
beside `cordis:include` loads both through the ambient module pipeline
instead.

Record it in the preset Agent Note, which leans on the realm vocabulary
without saying where the group row comes from, and drop the preset
README's limitation claiming this builtin is unavailable — it described
the state this change ends.

The test's assertion had a vacuous escape: `provide` mints the root
symbol unconditionally, so the `rootKey === undefined` disjunct could
never hold and the comment claiming the root realm never learned the
name was wrong. Pin both halves — the symbol exists, nothing is stored
under it — and clean up the global the fixture writes.
This commit is contained in:
Yichen Jiang
2026-08-06 11:57:00 +08:00
parent 065257addb
commit e27d38efd6
15 changed files with 82 additions and 14 deletions
@@ -29,6 +29,8 @@ Mounting is per-session by default. Measured cost for a twelve-row composition i
**A directly-plugged subtree is invisible to the boot audit.** It never links itself to an `Entry`, so it is absent from `ctx.loader.entries()` and `assertEntriesActivated` cannot see it. The mount audits its own rows instead, reading the tree through an `Include` subclass that publishes it.
**A preset can only name a group because the app registers one.** Sharing a realm across rows is a `cordis:group` row, and a preset living outside this workspace — the authored ones under the Harness home, which is the point — cannot resolve `@cordisjs/plugin-group` by name: Node's upward `node_modules` walk never reaches the harness from there. `boot()` therefore registers `cordis:group` beside `cordis:include` as a loader builtin, so both load through the ambient module pipeline rather than through the included tree's own specifier resolution. Without it the `isolate` vocabulary above is expressible one row at a time only, and a provider could never be grouped with its consumers.
**A preset may not publish into the root service realm.** Such a service is process-global rather than per-session, so the second session mounting the same preset collides with the first — and the collision surfaces as an unhandled rejection that `setup` never observes, leaving a half-composed agent that looks healthy. The mount rejects it instead, and the package invariant re-checks on every service notification because a row publishing from a timer or an asynchronous continuation would escape a one-shot audit.
**Failure rolls the agent back.** `setup` runs before publication, so a rejected mount fails `ctx.agents.create()` and leaves nothing behind. This is why `setup` is the one supported call site.