Make the module navigation capability an optional context member
CI / verify (pull_request) Successful in 1m11s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m27s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 52s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m35s

ModuleUiHostContext.navigation is now `navigation?: ModuleUiNavigation |
undefined`, so the public context change is source-compatible: a context built
before navigation existed (without the member) still compiles unchanged, as
0.x patch releases require. The Dashboard supplies navigation; hosts without
addressable pages, including the display host, omit the member; modules
feature-detect it. Existing hosts' tests are restored to their pre-PR shape,
and a module-ui-sdk test proves a pre-navigation context still satisfies
ModuleUiHostContext (and fails if the member is ever made required).
Runtime behaviour, manifest v8/v9 compatibility, and security rules are
unchanged. ADR 0063 and the module development guide say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-15 09:13:03 -05:00
co-authored by Claude Opus 5.5
parent ae360ef318
commit c78f1318b4
13 changed files with 60 additions and 37 deletions
+3 -3
View File
@@ -274,7 +274,7 @@ export interface CreateModuleUiContextOptions {
* renderer without a real network. Production composition leaves it unset. */
readonly importModule?: (url: string) => Promise<unknown>;
/** The module's navigation capability (ADR 0063), from a host whose pages are addressable — the
* Dashboard. Omitted, the context carries `navigation: undefined`. */
* Dashboard. Omitted, the context omits the member, as it did before navigation existed. */
readonly navigation?: ModuleUiNavigation;
}
@@ -361,8 +361,8 @@ export function createModuleUiContext(options: CreateModuleUiContextOptions): Mo
moduleId: options.descriptor.id,
permissions: Object.freeze({ has: (permissionId: string) => granted.has(permissionId) }),
// Only a host with addressable module pages supplies navigation (ADR 0063); the display host
// shows one screen and supplies none.
navigation: options.navigation,
// shows one screen and omits the member entirely, exactly as before navigation existed.
...(options.navigation === undefined ? {} : { navigation: options.navigation }),
principal: options.principal,
realtime: createModuleRealtimeClient(options.config, options.descriptor),
});
+2 -3
View File
@@ -251,13 +251,12 @@ describe("display host — full-viewport boundary", () => {
"contributions",
"display",
"moduleId",
"navigation",
"permissions",
"principal",
"realtime",
]);
// A display is one screen with no pages: the display host offers no navigation (ADR 0063).
expect(seenContext?.navigation).toBeUndefined();
// A display is one screen with no pages: the display host omits navigation (ADR 0063).
expect(seenContext && "navigation" in seenContext).toBe(false);
expect(seenContext?.principal).toEqual({ kind: "authenticated", userId: "user-1" });
expect(isolatedText(query("display-module-container"))).toBe("permitted");
expect(
+4 -4
View File
@@ -569,7 +569,7 @@ describe("module UI host context", () => {
expect(context.display?.url).toBe("https://api.example.test/display/example");
});
it("exposes exactly the eight contract members and the browser principal", () => {
it("exposes exactly the seven contract members and the browser principal", () => {
const { implementation } = recordingFetch();
const context = contextFor(implementation);
@@ -579,14 +579,14 @@ describe("module UI host context", () => {
"contributions",
"display",
"moduleId",
"navigation",
"permissions",
"principal",
"realtime",
]);
expect(context.moduleId).toBe("example");
// Navigation comes only from a host with addressable pages (ADR 0063); this one supplies none.
expect(context.navigation).toBeUndefined();
// Navigation comes only from a host with addressable pages (ADR 0063). Without it the member is
// omitted, so the context is exactly the shape it was before navigation existed.
expect("navigation" in context).toBe(false);
expect(context.principal).toEqual({ kind: "authenticated", userId: "user-1" });
});